Multithreading
Parallel Algorithms
David Mayerich
Scalable Tissue Imaging and Modeling (STIM) Laboratory
Department of Electrical and Computer Engineering
Cullen College of Engineering
University of Houston
David Mayerich
STIM Laboratory, University of Houston

Memory Allocation
-
stack vs. heap
-
A stack is a data structure that is very fast at removing stuff added last
-
Basic allocations happen on the stack:
David Mayerich
STIM Laboratory, University of Houston
int a = 12;
int b = 300;
float x = 3.14;
float y[3];
float* z = (float*) malloc(sizeof(float) * 3);previous allocations
a = 12
b = 300
x = 3.14
y[0]
y[1]
y[2]
z*

z[0]
z[1]
z[2]
heap
Practical Stack vs. Heap Allocation
-
The stack is fast - resets with scope:
David Mayerich
STIM Laboratory, University of Houston
int N = 100;
float z = 0.0;
for(int i = 0; i < N; i++) {
float x = sin( a[i] );
float y = cos( b[i] );
z = x * y;
}-
The stack has a limited size (stack overflow)
-
The heap is slower to allocate/free
previous allocations
N = 100
z = 0.0
i = 0
x
y

Dynamic Allocation
-
DO NOT use variables in static allocations:
-
Not supported in recent versions of the C standard
-
Can result in a stack overflow
-
David Mayerich
STIM Laboratory, University of Houston
-
DO use dynamic allocation:
int n;
scanf("size of array: %d", n);
float A[n];float* A = (float*) malloc( n * sizeof(float));OR (using C++):
float* A = new float[n];Segmentation Faults
David Mayerich
STIM Laboratory, University of Houston
int N = 2000;
//.....
double* x;
x = (double*) malloc(N * sizeof(double));
x[2000] = 3.14159; // possible segmentation faultHeap Memory
Case 1 - allocation is contiguous in memory
Case 2 - allocation is not contiguous in memory
A[6] overwrites B[0]
(no error reported)
segmentation fault
A[0]
B[0]
A[0]
B[0]
Multidimensional Arrays
-
Stack allocation for 2D arrays:
David Mayerich
STIM Laboratory, University of Houston
-
Heap allocation for indirect addressing:
double A[20][20];
A[7][3] = 7.0 * 3.0;int M = 20; int N = 20;
//.....
double** A;
A = (double**) malloc(M * sizeof(double*));
for(int i = 0; i < M; i++) {
A[i] = (double*) malloc(N * sizeof(double));
}
A[7][3] = 7.0 * 3.0;A[N][0]
A[N][1]
A[N][N-1]
A[1][0]
A[1][1]
A[1][N-1]
double*double**A
doubleMultidimensional Arrays
-
Indirect addressing is slower
-
requires following a pointer in the heap
-
difficult to cache
-
-
Heap allocation for direct addressing:
David Mayerich
STIM Laboratory, University of Houston
int M = 20; int N = 20;
//.....
double* A;
A = (double*) malloc(M * N * sizeof(double*));
A[7 * N + 3] = 7.0 * 3.0;
integer math is fast
A
A[0]
A[1*M+0]
A[1*M+2]
A[(N-1)*M+0]
Storing Matrices
-
Matrices are just 2D arrays, but they're very common and have their own terminology:
-
row major order format stores elements such that row values are contiguous
-
column major order format stores elements such that column values are contiguous
-
David Mayerich
STIM Laboratory, University of Houston
Row-Major Order
Column-Major Order
Higher Dimensions
-
Three dimensions \((x, y, z)\):
David Mayerich
STIM Laboratory, University of Houston
int X = 20; int Y = 30; int Z = 10;
float* A = (float*) malloc(X * Y * Z * sizeof(float));
// access (7, 8, 9):
A[9 * X * Y + 8 * X + 7] = 7.0f * 3.0f;Function Pointers
-
Functions, like data, have locations in memory
-
If we define two functions with similar signature:
David Mayerich
STIM Laboratory, University of Houston
double cosine_expansion(double x) {
const double x2 = x * x;
double term = 1.0;
double sum = 1.0;
for (int k = 1; k < max_terms; ++k) {
term *= -x2 / ((2.0 * k - 1.0) * (2.0 * k));
if (sum + term == sum) break;
sum += term;
}
return sum;
}double sine_expansion(double x) {
const double x2 = x * x;
double term = x;
double sum = x;
for (int k = 1; k < max_terms; ++k) {
term *= -x2 / ((2.0 * k) * (2.0 * k + 1.0));
if (sum + term == sum) break;
sum += term;
}
return sum;
}-
These functions can be selectively called:
double (*trig)(double); // create a function pointer "trig"
if(user_input == "cos") // if the user wants to call cosine
trig = cosine_expansion; // assign that function to trig
else trig = sine_expansion; // otherwise assign the sine function
double y = trig(x); // call the user-specified trig functionCallback Functions
-
These are useful for creating callback functions: user-specified functions called by non-user-specified functions
David Mayerich
STIM Laboratory, University of Houston
// user-defined function that prints the
// mouse coordinates
void user_mouse_click(int px, int py){
printf("Mouse coordinate: %d, %d", px, py);
}Callback Functions
-
These are useful for creating callback functions: user-specified functions called by non-user-specified functions
David Mayerich
STIM Laboratory, University of Houston
// user-defined function that prints the
// mouse coordinates
void user_mouse_click(int px, int py){
printf("Mouse coordinate: %d, %d", px, py);
}
// tell the GUI (someone else's code)
// what function to call when the mouse
// is clicked
GUI.set_mouse_func(user_mouse_click);Threads and Concurrency
-
A thread is a lightweight process that manages an independent set of instructions
-
Each thread has its own call stack, including registers
-
The main thread is the execution pipeline for
main()and used to launch and control other concurrent threads:
David Mayerich
STIM Laboratory, University of Houston
execution time
shared state
heap
libraries
global functions
main()
registers
call stack:
inst 2
launch t0
inst 3
launch t1
inst 4
...
thread0()
registers
call stack:
inst a1
inst a2
...
thread1()
...
C++ Thread Library
-
C++11 standard supports threads
-
Launch independent threads by assigning a function pointer to the constructor:
David Mayerich
STIM Laboratory, University of Houston
#include <thread>
void func(float p1, int p2, ...) {
// do independent work using
// p1, p2, etc.
}
// ...
// create a thread by passing:
// 1) the function to execute
// 2) all parameters required by the function
std::thread t(func, p1, p2, ...);Matrix-Vector Multiplication (Example)
-
Consider the following operation:
David Mayerich
STIM Laboratory, University of Houston
-
This can be computed as a set of \(N\) dot (inner) vector products:
Inner Product Implementation
David Mayerich
STIM Laboratory, University of Houston
// Calculate the dot (inner) product of two vectors v1 and v2
// of length N and store the result in c
void dot(float* v1, float* v2, float* c, float N) {
}Inner Product Implementation
David Mayerich
STIM Laboratory, University of Houston
// Calculate the dot (inner) product of two vectors v1 and v2
// of length N and store the result in c
void dot(float* v1, float* v2, float* c, float N) {
// create a local register to accumulate the result
float r = 0;
}Inner Product Implementation
David Mayerich
STIM Laboratory, University of Houston
// Calculate the dot (inner) product of two vectors v1 and v2
// of length N and store the result in c
void dot(float* v1, float* v2, float* c, float N) {
// create a local register to accumulate the result
float r = 0;
// for each element in both vectors
for(int i = 0; i < N; i++)
}Inner Product Implementation
David Mayerich
STIM Laboratory, University of Houston
// Calculate the dot (inner) product of two vectors v1 and v2
// of length N and store the result in c
void dot(float* v1, float* v2, float* c, float N) {
// create a local register to accumulate the result
float r = 0;
// for each element in both vectors
for(int i = 0; i < N; i++)
r += v1[i] * v2[i];
}Inner Product Implementation
David Mayerich
STIM Laboratory, University of Houston
// Calculate the dot (inner) product of two vectors v1 and v2
// of length N and store the result in c
void dot(float* v1, float* v2, float* c, float N) {
// create a local register to accumulate the result
float r = 0;
// for each element in both vectors
for(int i = 0; i < N; i++)
r += v1[i] * v2[i];
*c = r;
}Implement Single-Process Multiplication
-
Define the dot product function (previous slide):
David Mayerich
STIM Laboratory, University of Houston
void dot(float* v1, float* v2, float* c, float N);-
Single-process implementation of \(\mathbf{y}=\mathbf{Mx}\):
// allocate memory
float* x = new float[N];
float* y = new float[N];
float* M = new float[N*N];
// fill values x, M ...Implement Single-Process Multiplication
-
Define the dot product function (previous slide):
David Mayerich
STIM Laboratory, University of Houston
void dot(float* v1, float* v2, float* c, float N);-
Single-process implementation of \(\mathbf{y}=\mathbf{Mx}\):
// allocate memory
float* x = new float[N];
float* y = new float[N];
float* M = new float[N*N];
// fill values x, M ...
// iterate across all matrix rows
for(int ri = 0; ri < N; ri++)
dot(x, &M[ri * N], &y[ri], N);Implement Multithreading
-
Define the dot product function (previous slide):
David Mayerich
STIM Laboratory, University of Houston
void dot(float* v1, float* v2, float* c, float N);-
Single-process implementation of \(\mathbf{y}=\mathbf{Mx}\):
// allocate memory
float* x = new float[N];
float* y = new float[N];
float* M = new float[N*N];
// fill values x, M ...
// create an array of threads
std::thread* t = new std::thread[N];
//launch one thread for each row
for(int ri = 0; ri < N; ri++)
t[ri] = std::thread(dot, y, &M[ri * N], &x[ri], N);Synchronization
-
This loop will return immediately
-
HOWEVER,
ywill be invalid until all threads complete -
Implement a synchronization barrier with
join():
David Mayerich
STIM Laboratory, University of Houston
for(int ri = 0; ri < N; ri++)
t[ri] = std::thread(dot, y, &M[ri * N], &x[ri], N);for(int ri = 0; ri < N; ri++)
t[ri] = std::thread(dot, y, &M[ri * N], &x[ri], N);
// synchronization barrier -----------
for(int ri = 0; ri < N; ri++)
// join() returns when the thread is finished
t[ri].join();Summary
-
By default, a C/C++ program is operating along a single
mainthread -
Child threads can be spawned using an
std::threadobject-
Create the
std::thread -
Pass the work to the thread's constructor as a function pointer (
*func)
-
-
Child threads have their own state space: parameters passed to
functhrough the constructor are unique to that thread -
Child threads can access global memory via pointers
-
When a thread is launched, the main thread returns immediately: you can't expect the thread to be finished without checking!
-
Threads can be synchronized with
mainusing thejoin()function
David Mayerich
STIM Laboratory, University of Houston
D.1 Multithreading
By STIM Laboratory
D.1 Multithreading
- 235