Unit IV — Programming Models & Tools

OpenMP in Depth

Session 10 • 2311CSC501J — Parallel Processing • Lab 1: Matrix Multiplication

What You'll Learn

  • Work-sharing: for, sections, single
  • Scheduling: static / dynamic / guided
  • Data scoping — the #1 OpenMP bug source
  • Lab 1: parallel matrix multiplication

"OpenMP is the gentlest way into parallel programming: you write ordinary loops, add one #pragma, and the compiler wakes up every core."

— the whole session in one line

Recap: Fork-Join & Parallel Regions

#include <omp.h>
#include <stdio.h>

int main(void) {
    printf("Before: one master thread\n");   // serial

    #pragma omp parallel                      // FORK a team
    {
        int id = omp_get_thread_num();        // 0, 1, 2, ...
        printf("Hello from thread %d\n", id); // runs on every thread
    }                                         // JOIN back to one

    printf("After: serial again\n");          // serial
    return 0;
}

Set the team size at run time with OMP_NUM_THREADS=8 ./program — no recompile. Compile everything today with gcc -fopenmp.

Work-Sharing: Splitting the Job

A parallel region alone makes every thread do the same work. Work-sharing constructs divide the work up:

Construct What it does Flavour
omp forSplits loop iterations across the teamData parallel
omp sectionsRuns several different code blocks in parallelTask parallel
omp singleBlock runs on exactly one thread (any one)One-off work
omp masterBlock runs on the master thread onlyOne-off work
// The combined shortcut you'll use most: parallel + for in one line
#pragma omp parallel for
for (int i = 0; i < N; i++) {
    a[i] = b[i] + c[i];     // each thread gets a slice of the iterations
}

Task Parallelism with sections

for is the same work over many data. sections is different jobs at once — one cook chops, one boils rice, one fries.

#pragma omp parallel
{
    #pragma omp sections          // the team splits up the sections
    {
        #pragma omp section
        { load_data(); }          // one thread runs this

        #pragma omp section
        { render_report(); }      // another thread runs this

        #pragma omp section
        { send_email(); }         // another thread runs this
    }                             // implicit barrier: wait for all three
}

Each section runs on exactly one thread. More sections than threads? Some threads run two, back-to-back.

There's an implicit barrier at the end. Add nowait to skip it if you don't need to wait.

Scheduling: Who Gets Which Iterations?

schedule(kind [,chunk]) decides how loop iterations map to threads. This ties directly to load balancing (Session 09).

Kind How it assigns work Use when
staticEqual, fixed chunks decided up front. No coordination cost.Every iteration costs the same
dynamicThreads grab the next chunk when they finish. Self-balancing.Uneven / unpredictable work
guidedLike dynamic, but chunks start big and shrink.Uneven, want less overhead

Trade-off: static is cheapest but leaves threads idle when work is lumpy. dynamic keeps everyone busy but pays a coordination cost each time a thread grabs work. Even work → static; uneven work → dynamic/guided.

Data Scoping — The #1 Bug Source

When threads share a variable they shouldn't, you get a silent, luck-dependent wrong answer. Scoping clauses say who sees what:

Clause Meaning
shared(x)One copy, all threads see it. Fine for read-only or disjoint writes.
private(x)Each thread gets its own uninitialised copy.
firstprivate(x)Private, but initialised from the value before the region.
lastprivate(x)Private, but the last iteration's value is copied back out.
default(none)Forces you to declare the scope of every variable.

Hygiene tip: always write default(none). It's extra typing, but the compiler then refuses to build until you've thought about every variable — catching scoping bugs before they run.

Synchronization: Taking Turns Safely

critical

Only one thread at a time may enter the block. General but relatively slow.

atomic

A single memory update (like x += v) done safely, using a fast hardware instruction.

barrier

Every thread waits here until all have arrived, then all continue together.

reduction(+:sum)

Each thread keeps a private partial; OpenMP combines them safely at the end. Fast and correct.

long sum = 0;
#pragma omp parallel for reduction(+:sum)   // the right way to combine
for (int i = 0; i < N; i++) sum += a[i];   // no race, correct every run

nowait removes the implicit barrier at the end of a work-sharing loop — a speed win when the next block doesn't depend on this one finishing.

Lab 1: Matrix Multiplication

Multiplying two matrices, C = A × B, is the textbook "embarrassingly parallel" problem: every output cell is an independent dot product. No two cells depend on each other — so they can all be computed at once.

C[i][j] = A[i][0]*B[0][j] + A[i][1]*B[1][j] + ... + A[i][n-1]*B[n-1][j]

        A (rows)              B (cols)            C (independent cells)
     ┌───────────┐        ┌───┬───┬───┐        ┌───┬───┬───┐
row i│ ● ● ● ● ● │   ×    │ │ │ │ │ │ │   =    │ · │ ✦ │ · │  ← each ✦ is
     └───────────┘        │ │ │ │ │ │ │        ├───┼───┼───┤    its own job,
                          │ ▼ │ ▼ │ ▼ │        │ · │ · │ · │    computed by
                          └───┴───┴───┘        └───┴───┴───┘    any thread

The plan: parallelise the outer (row) loop, so each thread owns a band of rows and fills them independently. One #pragma, and the work spreads across every core.

Lab 1: The Serial Version

The classic triple loop, running on one thread. This is our baseline — we time it, then race the parallel version against it.

void multiply_serial(const double *A, const double *B, double *C) {
    for (int i = 0; i < N; i++) {          // each row
        for (int j = 0; j < N; j++) {      // each column
            double sum = 0.0;              // dot product accumulator
            for (int k = 0; k < N; k++) {  // walk the row x column
                sum += A[i*N + k] * B[k*N + j];
            }
            C[i*N + j] = sum;              // one output cell
        }
    }
}

double t0 = omp_get_wtime();               // high-resolution timer
multiply_serial(A, B, C);
double serial_time = omp_get_wtime() - t0; // seconds

omp_get_wtime() returns wall-clock seconds and ships with OpenMP — the right tool for measuring parallel speedup.

Lab 1: The Parallel Version — One Line

The maths is identical. We add one pragma on the outer loop — and get the scoping right.

void multiply_parallel(const double *A, const double *B, double *C) {
    #pragma omp parallel for schedule(static) \
            default(none) shared(A, B, C)
    for (int i = 0; i < N; i++) {          // i is private (the omp for index)
        for (int j = 0; j < N; j++) {      // j declared here -> private
            double sum = 0.0;              // sum is local -> private
            for (int k = 0; k < N; k++) {  // k declared here -> private
                sum += A[i*N + k] * B[k*N + j];
            }
            C[i*N + j] = sum;              // threads write disjoint rows: no race
        }
    }
}

Why no race? Each thread writes its own rows of C. No two threads touch the same cell.

The trap: if j or k were shared, threads would corrupt each other's inner loop. Declaring them inside makes them private.

Lab 1: Results & Speedup

Same 512×512 multiply, just changing OMP_NUM_THREADS (measured on an 8-core laptop):

Threads Time Speedup Efficiency
1 (serial)0.150 s1.0×100%
40.044 s3.4×86%
80.026 s5.6×70%

Why not a perfect 8×? Memory bandwidth (all threads hammer the same RAM), cache misses (reading B by column is cache-unfriendly), fork/join overhead, and turbo/thermal limits. This is Amdahl's Law and coordination cost (Session 08) made concrete — efficiency drops as you add cores.

Pitfalls & Good Habits

Common bugs

  • A loop variable left shared → corruption
  • Updating a shared total without reduction → a data race
  • schedule(dynamic) with a tiny chunk → overhead swamps the work
  • Forgetting a matrix is too big for the stack (use static/malloc)

Good habits

  • Always start with default(none)
  • Prefer reduction over hand-rolled critical
  • Compile with -O2; measure with omp_get_wtime
  • Match the schedule to the work: even→static, uneven→dynamic

The CTO framing: OpenMP is the "just make this loop use all the cores" tool. Scoping bugs are the same shared-mutable-state bugs that bite every backend engineer — two workers writing the same thing without coordinating.

Recap & What's Next

Key Takeaways

Homework

Next session: MPI — Message Passing (Lab 2: Parallel Sort)

OpenMP stops at one machine. MPI is how thousands of machines cooperate.