Understanding Data Dependencies in Programming
Understanding Data Dependencies in Programming
Statements:
1. A = 1.0
2. B = A + C - 2.0
Statement 1: A = 1.0
Statement 2: B = A + C - 2.0
Summary:
If two statements (let’s call them S1 and S2) use or define the same variable, then they
depend on each other in some way. This is called data dependence.
When it happens:
A variable is defined in S1 and used in S2.
What it means:
S2 needs the result from S1 to do its work.
Example:
S1: A = B + C;
S2: D = 2 * A;
o A is defined in S1.
o A is used in S2.
So this is a true dependence.
2. Anti Dependence
When it happens:
A variable is used in S1 and then defined in S2.
What it means:
The second statement overwrites a variable that was used earlier.
Example:
S1: A = B + C;
S2: B = 0;
o B is used in S1.
o B is defined (assigned) in S2.
This is anti dependence because S2 changes a variable that S1 already used.
3. Output Dependence
When it happens:
A variable is defined in both S1 and S2.
What it means:
Two statements are writing to the same variable.
Example:
S1: A = B + C;
S2: A = A - D;
o A is defined in both S1 and S2.
So this is output dependence.
4. Input Dependence
When it happens:
A variable is used in both S1 and S2, but not modified.
What it means:
Both statements are just reading the same variable — no conflict.
Example:
S1: X = A + B;
S2: Y = B - C;
o B is used in both, but not changed.
So this is input dependence.
Summary Table
Safe for
Type DEF & USE overlap What’s happening
DEF(S1) ∩ USE(S2) ≠ ∅ S2 needs result from S1
parallel?
Problem:
Do I = 0 to N-1
A[2*I + 1] = B[I]
D[I] = A[2*I]
End Do
Key idea:
When dealing with arrays, you can’t just treat the whole array as one variable for
dependence. Instead, you need to look at which specific elements (indexes) of the arrays are
being accessed.
Why?
Because different iterations of the loop might read or write different elements of the array.
Some elements might overlap (cause dependence), others might not.
2*I + 1 = 2*J
Rearrange:
2*I + 1 = 2*J
=> 1 = 2*J - 2*I
=> 1 = 2*(J - I)
Conclusion:
Summary:
It’s a way to check if different iterations of a loop depend on each other or not.
Can processor A work on iteration i=1, while processor B works on iteration i=2 at the
same time?
How do we check?
1. Look at each statement inside the loop.
2. Find which variables are defined (DEF) and which are used (USE).
3. Check if the same variable (or array element) is involved in different iterations.
Important:
For arrays, check if the subscript (index) overlaps between different iterations.
If two iterations access the same memory location and one writes, the other reads or
writes, then they depend on each other and cannot be done in parallel.
Step-by-step:
Imagine the loop has these two statements for each iteration i:
Simple example:
For i = 1 to N
A[i] = B[i] + 1; // S1: defines A[i]
C[i] = A[i-1] + 2; // S2: uses A[i-1]
End For
Summary:
Loop dependence analysis checks if loop iterations can run independently or not.
Use DEF and USE sets and consider array indices.
If two iterations access the same element, with at least one write, then they are
dependent.
If no overlapping access, iterations can run in parallel on different processors!
Program Transformations
1. Induction Variables
These are variables inside a loop whose values increase or decrease in a fixed pattern (like
arithmetic progression).
Example:
m = 0;
for (i = 1; i <= N; i++) {
m = m + k; // m increases by 'k' each time
x[m] = a[i];
}
Why important?
Compilers can use this property to optimize loops (e.g., replacing repeated addition
with multiplication).
2. Forward Dependency
When the result of the next iteration depends on the current iteration’s value.
Example:
3. Backward Dependency
When the result of the current iteration depends on the previous one’s result.
Example:
Example:
This is tricky for parallel execution because you don’t know in advance where it will stop.
5. Loop Splitting
Instead of doing multiple tasks in one loop, we split them into separate loops.
This often makes loops easier to optimize or parallelize.
Example 1:
// Original
for (i = 1; i <= N; i++) {
a[i] = b[i] + c[i];
c[i] = a[i-1];
}
// Split version
for (i = 1; i <= N; i++) {
a[i] = b[i] + c[i];
}
for (i = 1; i <= N; i++) {
c[i] = a[i-1];
}
Example 2:
// Original
for (i = 1; i <= N; i++) {
a[i] = b[i] + c[i];
c[i] = a[i+1];
}
// Split version
for (i = 1; i <= N; i++) {
x[i] = c[i];
c[i] = a[i+1];
}
for (i = 1; i <= N; i++) {
a[i] = b[i] + c[i];
}
Here, splitting makes the loop more structured and avoids conflicts.
In summary:
6. Loop Interchange
Basic Example
// Original
for (i = 1; i <= N; i++) { // Outer loop
for (j = 1; j <= M; j++) { // Inner loop
A[i][j] = A[i][j] + 1;
}
}
// After interchange
for (j = 1; j <= M; j++) { // Outer loop
for (i = 1; i <= N; i++) { // Inner loop
A[i][j] = A[i][j] + 1;
}
}
Both give the same result, but performance may change.
1. Cache Efficiency
Example:
2. Parallelization
Example:
// With dependency
for (i = 1; i <= N; i++) {
for (j = 1; j <= N; j++) {
A[i][j] = A[i-1][j] + 1; // depends on previous row
}
}
3. Vectorization
Modern CPUs can process multiple elements at once (SIMD).
Loop interchange can arrange data so CPUs can vectorize more easily.
In Summary:
1. Any process can wait for an arbitrary amount of time between any
two instructions
This means:
When multiple processes (programs) are running, the operating system may pause
one process at any time.
So, a process might stop after one line of code, and another process might run in
between.
Example:
This shows that execution is not continuous—there can be gaps (waiting times) between
instructions.
Looks simple, right? But at the machine level, it’s actually several instructions:
Process 1: a = a + 1
Process 2: a = a + 1
If a = 5 initially…
1. Process 1 reads a = 5.
2. Process 2 reads a = 5 (before Process 1 stores the update).
3. Process 1 adds 1 → result = 6, stores 6.
4. Process 2 adds 1 → result = 6, stores 6.
This is called a race condition and happens because a = a + 1 was not atomic.
In summary:
id = create_process(N);
switch(id) {
case 0: // parent
Do job 1;
break;
case 1: // child 1
Do job 2;
break;
case 2: // child 2
Do job 3;
break;
...
}
Analogy: Imagine a teacher (parent process) telling each student (child process) to solve a
different question in parallel.
Join_process(N, id);
This ensures that all parallel tasks are done before moving on.
Analogy: Teacher waits until all students submit their answers, then collects everything and
continues teaching.
Visibility of Data
When processes/threads run, the question is: Who can see what data?
1. Information Sharing
Processes (UNIX style): By default, each process has its own memory.
→ One process’s changes are not visible to others (unless special shared memory is
used).
Threads (within the same process):
→ All threads share the same memory.
→ Any change by one thread is immediately visible to others.
Analogy:
Processes = each student has their own notebook. They don’t see others’ notes unless
they share.
Threads = students writing in the same notebook, so everyone sees updates.
2. Shared Memory
Mutual Exclusion
When multiple processes/threads share memory, they may interfere with each other (race
condition).
To avoid this, we use locks.
Analogy:
In Summary:
Example:
Imagine you open a music player app on your computer. That music player is a process. It remembers
what song is playing (program counter) and has its own space to store data like your playlist (address
space).
Example:
In the music player, one thread might handle playing the music, while another thread shows the song
progress bar. Both threads share the playlist data in the same address space.
Example:
If you have two separate programs running on different computers, and you want them to work
together, you might use MPI to send data (like a chat message) from one to the other.
1. Synchronization: Making sure processes coordinate their actions correctly (like waiting for
each other before proceeding).
2. Data Movement: Actually sending data from one process’s memory to another’s.
Data Parallelism (SIMD – Single Instruction, Multiple Data)
Everyone is doing the same thing at the same time, but on different data.
SIMD means a single instruction is applied to multiple data items at once.
Example:
Imagine a group of workers peeling potatoes. Each worker peels one potato, but all of them are
doing the same action (peeling) at the same time.
In computing:
If you want to add 1 to every number in a list, data parallelism means:
Example:
In a restaurant kitchen:
One person is making pizza, another is cooking pasta, and another is making a salad.
Each person does a different task, working with different ingredients.
In computing:
One core is sorting a list, another is compressing an image, another is downloading a file — all at the
same time.
Example:
Let’s say a classroom of students gets the same assignment sheet, but each student is told to solve
different questions based on their row number.
They might finish at different times and don’t need to wait for each other.
Conce
Instructions Data Sync? Real-World Example
pt
Differe
SIMD Same Synchronized Everyone peels a potato at once
nt
Differe
MIMD Different Not necessarily One cooks, one washes, one chops
nt
Same Differe Not synced per
SPMD All follow same guide, do different parts
(program) nt operation
Example:
Process A sends a message to Process B → like Alice mailing a birthday card to Bob.
Example:
Process B checks its "mailbox" and finds the message from Process A → like Bob checking his
mailbox and finding Alice’s card.
Scatter:
Example:
You send a box of school supplies to your house. When it arrives:
In computing:
A process sends a list of data, and each part is placed into a different variable or memory location.
Gather:
The reverse of scatter. You collect data from multiple places and put it into one message to
send.
Like gathering all the ingredients from different kitchen shelves into one shopping basket.
Example:
You collect apples from the fridge, sugar from the pantry, and flour from the cabinet, and put them in
one basket to bring to your friend.
In computing:
A process gathers different pieces of data and sends them together in a single message.
Network Performance
It’s the time between sending the message and when the first byte arrives at the receiver.
Real-life example:
You send a letter. Latency is the time it takes before your friend sees the envelope in their mailbox.
It’s like the width of a highway: how many cars (or messages) can travel per second.
Higher bandwidth = more data can be sent at once.
Real-life example:
If you're mailing books, latency is how long it takes the first book to arrive, but bandwidth is how
many books you can send per hour.
Summary Table
Concept Analogy Simple Explanation
Send Mailing a letter Process sends a message to another
Receive Checking mailbox Process receives a message from another
Scatter Unpacking a box to different shelves One message goes to multiple memory spots
Data from many places packed into one
Gather Collecting from shelves into a box
message
Latency Time before a letter is delivered Delay before first data byte is received
Bandwidth Letters per hour How fast you can send lots of data
Real-Life Examples
You quietly walk over and take a book from your friend’s shelf.
They don’t need to hand it to you or even know right away.
You just pull the data when you need it.
Analogy:
Imagine a group of friends playing a game. Everyone has to wait at the starting line until all of them
are ready. Once everyone’s ready, they can all start together.
In computing:
If you have multiple processes running, a barrier will make all processes wait at the barrier point.
Once every process reaches it, they continue.
2. Broadcast (One-to-All)
Broadcast is when one process sends data to all other processes in a group.
Analogy:
Imagine you have one speaker in a classroom, and the speaker announces the same message to the
whole class at the same time.
In computing:
Process A has some data and needs to send it to all other processes in the group. Process A broadcasts
the data to everyone.
3. Multicast (One-to-Many)
Multicast is when one process sends data to many selected processes. It’s like a broadcast,
but the message goes to specific recipients, not everyone.
Analogy:
Imagine you’re a teacher, and you give handouts only to the students sitting in the front row, not the
whole class.
In computing:
Process A sends data to a specific set of processes (not all of them).
4. All-to-All
All-to-All is when every process sends data to every other process.
Analogy:
Imagine each person at a party tells their own unique story to everyone else in the room.
In computing:
Each process sends data to every other process. So, all processes are sending and receiving data
from each other.
5. Reduction (All-to-One)
Reduction is when all processes send data to one process, and then that one process
combines the data in some way (like summing up numbers, finding a max, etc.).
Analogy:
Imagine a group of friends collecting coins. Everyone puts their coins into a single basket, and then
one person counts all the coins.
In computing:
Every process contributes data (like a number) to one process, which then reduces it (adds,
multiplies, or performs another operation on it).
Analogy:
Imagine you’re in a large office with multiple departments. The message-passing library is like the
internal office communication system (phone, email, etc.) that helps all departments talk to each
other. It doesn’t tell you how to do your job, but it gives you the tools to communicate.
Analogy:
Think of a messaging system that doesn’t just send simple texts (like a basic phone call) but can also
handle group messages, files, video calls, and even conferences (more complex interactions). This
allows for a much broader range of tasks.
Analogy:
Imagine the library as a messaging app (like WhatsApp) – it’s the tool for communication, but you
still need to type out the message yourself (you choose the language) and press “send” (you decide
when to communicate).
For Parallel Computers, Clusters, and Heterogeneous Networks
It’s designed for systems with multiple computers or processors working together. These
can be:
o Parallel Computers: A single computer with multiple processors working in
parallel.
o Clusters: Multiple computers connected over a network working together.
o Heterogeneous Networks: Different types of systems (e.g., different hardware or
operating systems) working together.
Analogy:
You can think of it like a team of people working on different parts of a project. Some people are in
the same office (parallel computers), some are in different offices but connected by email (clusters),
and some people are in different cities with different tools (heterogeneous networks). The messaging
system helps them coordinate and communicate no matter where they are or what tools they’re
using.
Full-Featured
The library is designed to handle advanced communication tasks like high-performance
data exchange, synchronization, and complex computations. It’s robust and capable of
handling a variety of situations.
Analogy:
It’s like having a full-featured toolbox for all sorts of repairs. Whether you need to tighten a bolt,
hammer a nail, or fix a complex machine, you have the tools available.
Analogy:
Think of it as a community effort:
Portable
The library is designed to work on many different types of systems. It’s portable, meaning
the same code you write using this library can run on different computers, networks, or even
different kinds of hardware without changes.
Analogy:
You write a letter in a certain language, and it can be read by anyone across the world, no matter
what language they speak or what system they use (as long as they understand the language).
#include "mpi.h"
#include <stdio.h>
int main(int argc, char *argv[])
{
MPI_Init(&argc, &argv);
printf("Hello, world!\n");
MPI_Finalize();
return 0;
}
Explanation:
int main(int argc, char *argv[])
MPI_Init(&argc, &argv);
This initializes the MPI environment.
Must be called before any other MPI functions.
Prepares the program to run in parallel across multiple processes.
The &argc and &argv pass command-line info to MPI, allowing it to manage arguments if needed.
_______________________________________________________________________________
Since this program uses MPI, this message will be printed by every process running the program.
MPI_Finalize();
This cleans up and shuts down the MPI environment.
Must be called at the end of all MPI programs.
After this call, no MPI functions can be used.
This program initializes MPI, prints "Hello, world!" from every process (e.g., if you run with 4
processes, you’ll see 4 lines), then cleans up and exits.
#include "mpi.h"
#include <stdio.h>
int rank;
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // Get process ID (rank)
MPI_Finalize();
return 0;
}
Error Handling
You're diving into one of the most important aspects of MPI programming: error handling. In
distributed systems, a single failure can ripple across all processes—so how you handle errors can
make or break your application. Let’s unpack each statement in depth:
1. By Default, an Error Causes All Processes to Abort
MPI uses a default error handler called MPI_ERRORS_ARE_FATAL.
If any MPI function encounters an error (e.g., invalid communicator, out-of-range rank), this
handler calls MPI_Abort, which:
o Immediately terminates all processes in the communicator.
o Returns a non-zero exit code to the system.
Example:
MPI_Errhandler_set(MPI_COMM_WORLD, MPI_ERRORS_RETURN);
Now, instead of aborting, MPI functions will return an error code like MPI_ERR_COMM,
MPI_ERR_RANK, etc.
Benefits:
You can check the return value (ierr) and decide what to do:
o Log the error
o Retry the operation
o Skip the faulty part and continue
Example:
Example:
try {
int rank = MPI::COMM_WORLD.Get_rank();
} catch (MPI::Exception& e) {
std::cerr << "MPI Exception: " << e.Get_error_string() <<
std::endl;
}
Important Note:
These C++ bindings were deprecated in MPI-3 and are no longer recommended.
Most modern C++ MPI programs use C bindings with custom wrappers or exception logic.
Steps:
1. Define a handler function:
MPI_Errhandler errhandler;
MPI_Comm_create_errhandler(my_handler, &errhandler);
MPI_Comm_set_errhandler(MPI_COMM_WORLD, errhandler);
Use Cases:
Logging detailed diagnostics
Notifying external systems
Attempting recovery or fallback strategies
Example:
A matrix multiplication library might catch an MPI error internally and return a status code. The
calling application can then choose to retry, skip, or terminate.
Summary Table
Aspect Default Behavior Customizable? Best For
Error Handling Abort all processes Yes Critical failures
Return Codes No abort Yes Graceful recovery
C++ Exceptions Thrown (MPI-2) ❌ Deprecated Legacy C++
Custom Handlers User-defined Yes Logging, debugging
Library vs App Different strategies Yes Modularity vs control
If you're building a real-world MPI application, mastering error handling is essential. Want help
writing a custom error handler or simulating an MPI error to test your setup?
Basic MPI communication routines
1. MPI_Send
The function MPI_Send is used to send data from one process to another.
Syntax:
Parameters explained:
Behavior:
When you call MPI_Send, the function blocks until the system has safely stored or
delivered the message.
This means you can safely reuse or change the buffer (data_to_send) only after
MPI_Send returns.
So, it provides both data transfer and a bit of synchronization.
2. MPI_Recv
Syntax:
Parameters explained:
Behavior:
MPI_Recv blocks until the expected message arrives and is copied into the buffer.
After return, you can immediately use the received data.
3. Example
#include <mpi.h>
#include <stdio.h>
int rank;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if (rank == 0) {
int data = 42;
MPI_Send(&data, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);
printf("Process 0 sent %d\n", data);
} else if (rank == 1) {
int received_data;
MPI_Status status;
MPI_Recv(&received_data, 1, MPI_INT, 0, 0, MPI_COMM_WORLD,
&status);
printf("Process 1 received %d\n", received_data);
}
MPI_Finalize();
return 0;
}
Process 0 sent 42
Process 1 received 42
In short:
What is OpenMP?
Directive format
#pragma omp directive [clause list]
Important Points
1. Program starts in serial mode (single thread).
2. When it hits a parallel directive, multiple threads are created.
3. The main thread becomes the master thread (thread ID = 0).
4. Other threads are given their own IDs (1, 2, …).
5. After the parallel block, threads join back and execution continues serially.
In short:
OpenMP lets you turn normal code into parallel code just by adding special #pragma omp
lines. The compiler handles thread creation, synchronization, and shared data automatically.
We need a way to let each thread have its own local copy, and then combine them safely at
the end.
Example
#include <stdio.h>
#include <omp.h>
int main() {
int sum = 0;
sum=0+1+2+3=6sum = 0 + 1 + 2 + 3 = 6
In short:
reduction = private copies for each thread + safe combination at the end.
Prevents race conditions when accumulating results like sum, product, min, max, etc.
Parallel + For
The for directive is used when you want to run a for loop in parallel.
Each thread gets a portion of the loop iterations to execute.
You usually combine it with #pragma omp parallel → written as:
int main() {
int N = 8;
int arr[N];
Parallel + Sections
The sections directive is used when you want different tasks (not loop iterations) to
run in parallel.
Each section is assigned to a thread.
Useful when tasks are unrelated but can run at the same time.
In Simple Words:
When you use #pragma omp for, OpenMP needs to decide which thread runs which loop
iterations.
The schedule clause controls this distribution.
General form:
Types of Scheduling
1. Static Scheduling
Iterations are divided evenly among threads before the loop starts.
Each thread gets a fixed set of iterations.
If you specify a chunk, each thread gets chunk-sized blocks in a round-robin fashion.
Example:
Best for: loops where all iterations take about the same time.
2. Dynamic Scheduling
Example:
#pragma omp for schedule(dynamic, 2)
Each thread takes 2 iterations at a time, and when done, grabs more.
3. Guided Scheduling
Similar to dynamic, but chunks start large and get smaller over time.
Helps balance load while reducing overhead.
Example:
4. Runtime Scheduling
Example:
Summary Table
Schedule Type How Work is Assigned When to Use
In short:
That means all threads must wait until every thread finishes its assigned iterations
before moving on.
This is useful in many cases but sometimes it slows things down unnecessarily.
Threads that finish their work can move on immediately to the next directive.
Other threads will continue their work without blocking the fast ones.
Syntax:
#include <stdio.h>
#include <omp.h>
int main() {
char *list1[] = {"Alice", "Bob", "Charlie"};
char *list2[] = {"Eve", "Bob", "David"};
char *name = "Bob";
int found1 = 0, found2 = 0;
Without nowait:
o After the first for, all threads would stop and wait before moving to the
second list.
With nowait:
o Threads can immediately start checking the second list without waiting for
others to finish the first.
This saves time when loops are independent (like checking two different lists).
In short:
What is sections?
General Form
#pragma omp parallel
{
#pragma omp sections
{
#pragma omp section
{
// Task 1
}
Example
1. Compute factorial
2. Compute Fibonacci
3. Print a message
#include <stdio.h>
#include <omp.h>
int factorial(int n) {
int f = 1;
for (int i = 1; i <= n; i++) f *= i;
return f;
}
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
}
int main() {
#pragma omp parallel
{
#pragma omp sections
{
#pragma omp section
{
printf("Factorial(5) = %d (Thread %d)\n", factorial(5),
omp_get_thread_num());
}
Output (example)
Factorial(5) = 120 (Thread 1)
Fibonacci(6) = 8 (Thread 2)
Hello from thread 3!
Key Points
In short: Use sections when you have different independent tasks to run in parallel.
1. Barrier
All threads must wait until every thread reaches this point.
Ensures no thread goes ahead too far.
Example:
2. Single
Example:
Example:
4. Critical
Example:
5. Atomic
Example:
6. Nowait
Removes the implicit barrier at the end of constructs like for, single, or sections.
Lets threads move ahead without waiting for others.
Example:
Ensures that certain parts of a loop are executed in sequential order, even inside a
parallel loop.
Example:
Summary Table
Directive Meaning
In short: These constructs help control who executes what and ensure safe access to shared
data in OpenMP.
1. OMP_NUM_THREADS
export OMP_NUM_THREADS=8
Means: each #pragma omp parallel will start with 8 threads (unless overridden by
num_threads() clause).
2. OMP_DYNAMIC
export OMP_DYNAMIC=TRUE
3. OMP_NESTED
export OMP_NESTED=TRUE
Inner #pragma omp parallel will create its own team of threads instead of merging into
the outer team.
4. OMP_SCHEDULE
export OMP_SCHEDULE="static,4"
export OMP_SCHEDULE="dynamic,2"
In Linux/macOS (bash):
export OMP_NUM_THREADS=8
export OMP_DYNAMIC=FALSE
export OMP_NESTED=TRUE
export OMP_SCHEDULE="static,4"
In C Shell:
setenv OMP_NUM_THREADS 8
setenv OMP_DYNAMIC FALSE
setenv OMP_NESTED TRUE
setenv OMP_SCHEDULE "static,4"
Summary
Variable What it Does
Sets scheduling policy for loops (static, dynamic, etc.) when runtime is
OMP_SCHEDULE
used