Lesson 23-C Language Optimization Debugging and Code Style

C Language Optimization and Debugging

Code Optimization

The goal of code optimization is to improve program execution efficiency and resource utilization.

Example: Loop Unrolling

#include <stdio.h>
 Ejemplo: Despliegue de bucle
#include <time.h>

#define N 10000000

void loop_sum(int n) {
    long sum = 0;
    for (int i = 0; i < n; i++) {
        sum += i;
    }
    printf("Sum: %ld\n", sum);
}

void loop_sum_unrolled(int n) {
    long sum = 0;
    for (int i = 0; i < n; i += 4) {
        sum += i;
        sum += i + 1;
        sum += i + 2;
        sum += i + 3;
    }
    printf("Sum (unrolled): %ld\n", sum);
}

int main() {
    clock_t start, end;
    double cpu_time_used;

    start = clock();
    loop_sum(N);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Time taken by loop_sum: %.5f seconds\n", cpu_time_used);

    start = clock();
    loop_sum_unrolled(N);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Time taken by loop_sum_unrolled: %.5f seconds\n", cpu_time_used);

    return 0;
}

Compiler Optimization

The compiler provides various optimization options.

Example: Using -O3 Optimization

#include <stdio.h>
#include <time.h>

#define N 10000000

long loop_sum(int n) {
    long sum = 0;
    for (int i = 0; i < n; i++) {
        sum += i;
    }
    return sum;
}

int main() {
    clock_t start, end;
    double cpu_time_used;

    start = clock();
    long sum = loop_sum(N);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Sum: %ld\n", sum);
    printf("Time taken: %.5f seconds\n", cpu_time_used);

    return 0;
}

Compilation Command:

gcc -O3 -o sum sum.c

Memory Access Optimization

Optimize memory access patterns to reduce cache misses.

Example: Array Access Order

#include <stdio.h>
#include <time.h>

#define N 1000000

void row_major_access(int n, int a[n][n]) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            a[i][j] = i + j;
        }
    }
}

void column_major_access(int n, int a[n][n]) {
    for (int j = 0; j < n; j++) {
        for (int i = 0; i < n; i++) {
            a[i][j] = i + j;
        }
    }
}

int main() {
    int a[N][N];

    clock_t start, end;
    double cpu_time_used;

    start = clock();
    row_major_access(N, a);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Time taken by row_major_access: %.5f seconds\n", cpu_time_used);

    start = clock();
    column_major_access(N, a);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Time taken by column_major_access: %.5f seconds\n", cpu_time_used);

    return 0;
}

Debugging Techniques

Debugging is the process of discovering and fixing program errors.

Example: Using printf for Debugging

#include <stdio.h>

void swap(int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
    printf("Swapped values: x = %d, y = %d\n", *x, *y);
}

int main() {
    int x = 10, y = 20;
    printf("Before swap: x = %d, y = %d\n", x, y);
    swap(&x, &y);
    printf("After swap: x = %d, y = %d\n", x, y);

    return 0;
}

Using Debugging Tools

GDB is a commonly used debugging tool.

Example: Using GDB for Debugging

#include <stdio.h>

void divide(int a, int b) {
    if (b == 0) {
        printf("Error: Division by zero!\n");
        return;
    }
    int result = a / b;
    printf("Result: %d\n", result);
}

int main() {
    int a = 10, b = 2;
    divide(a, b);

    return 0;
}

Compilation Command:

gcc -g -o divide divide.c

Debugging Commands:

gdb ./divide

Unit Testing

Unit testing is used to verify the correctness of program modules.

Example: Using the Check Library for Unit Testing

#include <stdio.h>
#include <check.h>

int add(int a, int b) {
    return a + b;
}

START_TEST(test_addition) {
    ck_int_eq(add(1, 2), 3);
    ck_int_eq(add(-1, -1), -2);
}
END_TEST

Suite *suite_create(void) {
    Suite *s = suite_create("Addition Suite");
    TCase *tc = tcase_create("Addition Test Case");
    tcase_add_test(tc, test_addition);
    suite_add_tcase(s, tc);
    return s;
}

int main() {
    int failed = 0;
    SRunner *sr = srunner_create(suite_create());
    srunner_run_all(sr, CK_VERBOSE);
    failed = srunner_ntests_failed(sr);
    srunner_free(sr);
    return (failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}

Compilation Command:

gcc -o test test.c -lcheck

Performance Analysis

Performance analysis tools help identify bottlenecks.

Example: Using Valgrind for Performance Analysis

#include <stdio.h>
#include <time.h>

#define N 10000000

void loop_sum(int n) {
    long sum = 0;
    for (int i = 0; i < n; i++) {
        sum += i;
    }
    printf("Sum: %ld\n", sum);
}

int main() {
    clock_t start, end;
    double cpu_time_used;

    start = clock();
    loop_sum(N);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Time taken: %.5f seconds\n", cpu_time_used);

    return 0;
}

Performance Analysis Command:

valgrind --tool=callgrind ./sum

Static Analysis

Static analysis tools can detect potential issues at compile time.

Example: Using cppcheck for Static Analysis

#include <stdio.h>

int main() {
    int *ptr = NULL;
    *ptr = 10; // Attempting to assign to a null pointer
    return 0;
}

Static Analysis Command:

cppcheck --enable=all your_file.c

Dynamic Analysis

Dynamic analysis tools detect issues at runtime.

Example: Using Valgrind for Memory Leak Detection

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = malloc(10 * sizeof(int)); // Allocate memory but do not free it
    return 0;
}

Dynamic Analysis Command:

valgrind --leak-check=yes ./your_program

Concurrent Debugging

Concurrent debugging involves multi-threaded or multi-process programs.

Example: Using Helgrind for Concurrent Debugging

#include <stdio.h>
#include <pthread.h>

void *print_message(void *arg) {
    char *message = (char *)arg;
    printf("%s\n", message);
    pthread_exit(NULL);
}

int main() {
    pthread_t thread1, thread2;
    char *message1 = "Hello from thread 1!";
    char *message2 = "Hello from thread 2!";

    if (pthread_create(&thread1, NULL, print_message, (void *)message1) != 0) {
        perror("Thread 1 creation failed");
        return EXIT_FAILURE;
    }

    if (pthread_create(&thread2, NULL, print_message, (void *)message2) != 0) {
        perror("Thread 2 creation failed");
        return EXIT_FAILURE;
    }

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

Concurrent Debugging Command:

valgrind --tool=helgrind ./your_program

Logging

Logging helps track state changes during program execution.

Example: Using Logging

#include <stdio.h>
#include <time.h>

#define LOG(msg) fprintf(stderr, "[%s] %s\n", ctime(NULL), msg)

void process_data(int data) {
    LOG("Processing data...");
    // Process data
}

int main() {
    int data = 100;
    process_data(data);
    return 0;
}

Assertions

Assertions are used to verify whether assumed conditions hold true.

Example: Using Assertions

#include <assert.h>

void check_value(int value) {
    assert(value > 0);
    printf("Value is positive.\n");
}

int main() {
    int value = -1;
    check_value(value);
    return 0;
}

Memory Management

Proper memory management can prevent memory leaks.

Example: Using mtrace for Memory Tracking

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = malloc(0 * sizeof(int));
    free(ptr);
    return 0;
}

Compilation Command:

gcc -g -o your_program your_program.c -lmtrace

Memory Tracking Command:

mtrace ./your_program

Using Performance Analysis Tools

Performance analysis tools help identify bottlenecks in the program.

Example: Using perf for Performance Analysis

#include <stdio.h>
#include <time.h>

#define N 10000000

void loop_sum(int n) {
    long sum = 0;
    for (int i = 0; i < n; i++) {
        sum += i;
    }
    printf("Sum: %ld\n", sum);
}

int main() {
    clock_t start, end;
    double cpu_time_used;

    start = clock();
    loop_sum(N);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Time taken: %.5f seconds\n", cpu_time_used);

    return 0;
}

Performance Analysis Command:

perf record -e cycles:u ./your_program
perf report

Using Static Analysis Tools

Static analysis tools can detect potential issues at compile time.

Example: Using clang-tidy for Static Analysis

#include <stdio.h>

int main() {
    int *ptr = NULL;
    *ptr = 10; // Attempting to assign to a null pointer
    return 0;
}

Static Analysis Command:

clang-tidy your_file.c -checks=-*,bugprone-*,modernize-*,performance-*,readability-* -extra-arg=-std=c11

Using Dynamic Analysis Tools

Dynamic analysis tools detect issues at runtime.

Example: Using Valgrind for Memory Leak Detection

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = malloc(10 * sizeof(int)); // Allocate memory but do not free it
    return 0;
}

Dynamic Analysis Command:

valgrind --leak-check=yes ./your_program

Using Debuggers

Debuggers can help locate and fix errors in the program.

Example: Using lldb for Debugging

#include <stdio.h>

void divide(int a, int b) {
    if (b == 0) {
        printf("Error: Division by zero!\n");
        return;
    }
    int result = a / b;
    printf("Result: %d\n", result);
}

int main() {
    int a = 10, b = 2;
    divide(a, b);

    return 0;
}

Compilation Command:

clang -g -o divide divide.c

Debugging Command:

lldb ./divide

Using Performance Monitoring Tools

Performance monitoring tools can continuously monitor program performance metrics.

Example: Using top for Performance Monitoring

#include <stdio.h>
#include <time.h>

#define N 10000000

void loop_sum(int n) {
    long sum = 0;
    for (int i = 0; i < n; i++) {
        sum += i;
    }
    printf("Sum: %ld\n", sum);
}

int main() {
    clock_t start, end;
    double cpu_time_used;

    start = clock();
    loop_sum(N);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Time taken: %.5f seconds\n", cpu_time_used);

    return 0;
}

Performance Monitoring Command:

top -b -n 1 | grep your_program

Summary

  • Code Optimization: Loop unrolling, compiler optimization options, memory access pattern optimization.
  • Debugging Techniques: Using printf debugging, GDB debugging tools, unit testing.
  • Performance Analysis: Using valgrind, perf, and other tools for performance analysis.
  • Static Analysis: Using cppcheck, clang-tidy, and other tools for static analysis.
  • Dynamic Analysis: Using valgrind for memory leak detection.
  • Concurrent Debugging: Using helgrind for concurrent debugging.
  • Logging: Record state changes during program execution.
  • Assertions: Verify whether assumed conditions hold true.
  • Memory Management: Using mtrace for memory tracking.
  • Debuggers: Using GDB, LLDB, and other debuggers.
  • Performance Monitoring: Using top and other tools for performance monitoring.

C Language Code Style

Indentation and Alignment

  • K&R Style (Kernighan & Ritchie):

The left brace is placed at the end of the line, and the right brace aligns with the corresponding left brace.

  void example() {
      if (condition) {
          statement;
      }
  }
  • Allman Style (Eric Allman):

Each brace is on its own line, and the code block inside the braces is indented.

  void example() {
      if (condition) {
          statement;
      }
  }
  • Whitesmiths Style:

Similar to Allman style, but with an extra space before the left brace of each control structure.

  void example() {
      if ( condition ) {
          statement;
      }
  }

Naming Conventions

  • Snake Case (snake_case): Words in variable names are separated by underscores.
  int my_variable;
  • Camel Case (camelCase): The first word starts with a lowercase letter, and subsequent words start with an uppercase letter.
  int myVariable;
  • Pascal Case (PascalCase): Each word starts with an uppercase letter, commonly used for type names.
  int MyVariable;
  • Hungarian Notation: A prefix in the variable name indicates the data type.
  int nMyVariable;

Comments

  • Multi-line Comments (//): Used for longer comments.
  /* This is a multiline comment */
  • Single-line Comments (//): Used for short comments.
  // This is a single line comment
  • Documentation Comments (/ … */):** Usually placed before function declarations to provide documentation information.
  /**
   * @brief This function does something.
   * @param arg The argument to the function.
   * @return The result of the function.
   */
  int some_function(int arg);

Space Usage

Use spaces around operators.

  int a = 1 + 2;

Use a space after commas.

  int a, b, c;

Use a space between keywords and parentheses.

  if (condition) {
      // ...
  }

Function and Variable Declarations

Keep function declarations concise and clear.

  int add(int a, int b);

Use explicit variable types.

  unsigned int count;

Line Length Limit

It is generally recommended that each line does not exceed 80 characters.

  if (this_is_a_very_long_line_of_code_that_exceeds_the_standard_length_limit) {
      // ...
  }

File Structure

Include necessary header files.

  #include <stdio.h>
  #include "my_header.h"

Declare global variables and function prototypes at the top of the file.

  extern int global_var;
  int function(int arg);

Use appropriate naming conventions and comments.

  // File: example.c

  #include <stdio.h>
  #include "example.h"

  int global_var;

  int function(int arg) {
      // Function implementation
  }

Choosing an Appropriate Code Style Choosing a code style mainly depends on team habits and personal preferences. For team projects, it is best to choose one style and stick to it consistently, so that the code style of the entire project remains consistent. For example, the Linux kernel project mainly uses snake_case naming and K&R style.

Summary

  • K&R Style and Allman Style are the two most common brace placement methods.
  • Snake Case and Camel Case are the most commonly used variable naming methods.
  • Comments should be clear and useful, especially in complex code segments.
  • The use of spaces helps improve code readability.
  • Function and variable declarations should be concise and clear.
  • Line length limits help keep code tidy.
  • File structure should be clear and organized.

Share your love