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
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
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
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];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]
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
doubleIndirect 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]
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
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;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 functionThese 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);
}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);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++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, ...);Consider the following operation:
David Mayerich
STIM Laboratory, University of Houston
This can be computed as a set of \(N\) dot (inner) vector products:
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) {
}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;
}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++)
}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];
}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;
}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 ...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);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);This loop will return immediately
HOWEVER, y will 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();By default, a C/C++ program is operating along a single main thread
Child threads can be spawned using an std::thread object
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 func through 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 main using the join() function
David Mayerich
STIM Laboratory, University of Houston