2. Quick C++ Review

You’re likely no stranger to C++. Here we aim to quickly revisit key C++ concepts. Whether you’re an experienced developer or a newcomer, this refresher will help you strengthen your C++ knowledge and coding skills.

2.1. First Code Execution

To compile and run a C++ program in the Linux terminal, you will need a C++ compiler like g++, which is commonly available on most Linux distributions. Here are the general steps to compile and run a C++ program:

  1. Write Your C++ Code:

  • Use a text editor to create your C++ program. You can use popular text editors like Vi, Nano, or graphical code editors like VSCode. Save your C++ code in a file with the .cpp extension, e.g., my_program.cpp.

  • The “Hello, World!” program is a traditional and simple introductory program often used to demonstrate the basic syntax and structure of a programming language.

#include <iostream>

int main() {
  std::cout << "Hello, World!" << std::endl;
  return 0;
}
  1. Open Terminal:

  • Open your Linux terminal. You can usually do this by pressing Ctrl+Alt+T or by searching for “Terminal” in your desktop environment.

  1. Navigate to the Directory:

  • Use the cd (change directory) command to navigate to the directory where your C++ source code is located.

  1. Compile Your C++ Program:

  • Use the g++ compiler to compile your C++ program. The basic syntax is:

g++ -o <output_file> <input_file.cpp>
  1. Run Your Program:

  • Once your program is successfully compiled, you can run it in the terminal using:

    ./<my_program>
    
  1. View the Output:

  • If your C++ program includes output (e.g., using cout), you will see it displayed in the terminal.

Task

  1. Write a C++ program that prints “Hello, World!” to the console.

  2. Compile and execute the program.

  3. Verify that “Hello, World!” is displayed on the screen.

2.2. Optimization

GCC, the GNU Compiler Collection, provides various optimization options when using the g++ compiler for C++ code. These options allow you to improve the performance of your compiled code. Here are some common optimization options for g++:

  • -O0 (No Optimization): This is the default level, which means no optimization is applied. It’s useful for debugging and examining generated code.

  • -O1 (Optimize for Size): This level applies basic optimization techniques to reduce code size and improve execution speed. It’s suitable for most development and debugging tasks.

  • -O2 (Optimize More): This level includes additional optimization techniques, such as inlining functions and loop optimizations. It is the recommended level for most production code.

  • -O3 (Aggressive Optimization): This level applies further aggressive optimization, including inlining and auto-vectorization. It can significantly improve execution speed but may increase compilation time and code size.

To use these optimization options with g++, you would include them in your compile command. For example:

g++ -O2 [output_file] [input_file.cpp]

2.3. Datatype

Data types represent the kind of values that variables can store. Here are some common C++ data types:

  • int: Used for integer values, e.g., int age = 30;.

  • double: Used for floating-point numbers, e.g., double pi = 3.14159;.

  • char: Used for single characters, e.g., char grade = 'B';.

  • bool: Used for boolean values, e.g., bool isTrue = true;.

  • string: Used for sequences of characters, e.g., std::string name = "Shima".

  • float: Used for single-precision floating-point numbers, e.g., float price = 19.99f;.

  • unsigned int: Used for non-negative integers, e.g., unsigned int count = 95;.

2.4. I/O Control

In C++, cin and cout are used for standard input and output, respectively. Here’s a simple example that reads user input and displays it:

#include <iostream>

int main() {
  // Declare a variable to store user input
  float l_age;

  // Prompt the user to enter their age
  std::cout << "Please enter your age: ";
  std::cin >> l_age;

  // Display the entered age
  std::cout << "You entered: " << l_age << " years." << std::endl;

  return 0;
}

2.5. Arrays

An array is a collection of values stored in contiguous memory locations. 1D arrays are fundamental data structures in C++ that allow you to store and manipulate a collection of elements of the same data type. This section provides insights into their usage. The array elements are accessed using index notation, with the index starting from 0 and going up to (array size - 1). Remember to be cautious about accessing array elements to avoid going out of bounds, as this can lead to undefined behavior. To initialize, access and manipulate an array, you can use a loop or specify values directly as shown in the code example.

#include <iostream>

int main() {
  // Declare an array of integers with a fixed size
  const int l_s = 5;
  int l_arr[l_s];

  // Initialize the array with values
  for (int l_i = 0; l_i < l_s; l_i++) {
    l_arr[l_i] = l_i * 2;
  }

  // Access and print the values in the array
  std::cout << "Elements in the array: ";
  for (int l_i = 0; l_i < l_s; l_i++) {
    std::cout << l_arr[l_i] << " ";
  }

  return 0;
}

2.6. Dynamic Memory Allocation

In C++, when you use dynamic memory allocation to create objects such as arrays or matrices using the new operator, it is essential to use the delete operator to free the allocated memory when you’re finished with it. This is crucial for preventing memory leaks. Dynamic memory allocation in C++ allows you to allocate memory for variables at runtime.

It’s important to keep in mind that memory is flat, and data is stored continuously in memory. To access data in memory, you need to understand memory addresses. A memory address is a unique identifier for each byte of memory. In languages like C++, pointers are used to store and manipulate memory addresses. Pointers are vital for navigating flat memory, allowing you to access data at specific memory locations.

#include <iostream>

int main() {
  int l_r, l_c;
  std::cout << "Enter the number of rows: ";
  std::cin >> l_r;
  std::cout << "Enter the number of columns: ";
  std::cin >> l_c;

  // Allocate memory for a 2D matrix stored as a 1D array
  int* l_dm = new int[l_r * l_c];

  // Check if memory allocation was successful
  if (l_dm == nullptr) {
    std::cout << "Memory allocation failed." << std::endl;
    return 1;
  }

  // Input values into the dynamic matrix
  for (int l_i = 0; l_i < l_r; l_i++) {
    for (int l_j = 0; l_j < l_c; l_j++) {
      int l_index = l_i * l_c + l_j;
      std::cout << "Enter value for element (" << l_i << ", " << l_j << "): ";
      std::cin >> l_dm[l_index];
    }
  }

  // Display the elements of the dynamic matrix
  std::cout << "Elements in the dynamic matrix: ";
  for (int l_i = 0; l_i < l_r * l_c; l_i++) {
    std::cout << l_dm[l_i] << " ";
  }

  // Deallocate the memory
  delete[] l_dm;

  return 0;
}

Task

  1. Create random matrices A and B of varying sizes (e.g., 100x100, 500x500, 1000x1000).

  2. Write a function to perform matrix multiplication (GEMM):

  1. Row-Major GEMM:
    • Perform matrix multiplication in row-major order.

    • Measure the execution time.

  2. Column-Major GEMM:
    • Perform matrix multiplication in column-major order.

    • Measure the execution time.

  1. Record and compare execution times for both row-major and column-major implementations for each matrix size.

  2. Analyze the results and discuss the impact of memory layout on performance.