Open main menu

CDOT Wiki β

Changes

Team False Sharing

4,598 bytes added, 23:42, 21 December 2017
Identifying False Sharing
== Introduction ==
Multicore processors are more prevalent now more than ever, and Multicore programming is essential to benefit from the power of the hardware as it allows to run our code different CPU cores. But it is very important to know and understand the underlying hardware to fully utilize it. One of the most important system resources is the cache. And most architectures have shared cache lines. And this is why false sharing is a well know know problem in multicore/multithreaded processes.
'''What is False Sharing (aka cache line ping-ponging)?''' <br>
False Sharing is one of the sharing pattern that affect performance when multiple threads share data. It arises when at least two threads modify or use data that happens to be close enough in memory that they end up in the same cache line. False sharing occurs when they constantly update their respective data in a way that the cache line migrates back and forth between two threads' caches.
In this article, we will look at some examples that demonstrate false sharing, tools to analyze false sharing, and the two coding techniques we can implement to eliminate false sharing.
=Cache Coherence=
False sharing is a well-know performance issue on SMP systems, where each processor has a local cache. it occurs when treads on different processors modify varibles that reside on th the same cache line like so.
<br style="clear:both" />
[[File:CPUCacheline.png|center|frame]]
<br style="clear:both" />
The frequent coordination required between processors when cache lines are marked ‘Invalid’ requires cache lines to be written to memory and subsequently loaded. False sharing increases this coordination and can significantly degrade application performance.
<source lang="cpp">
#include <iostream>
#include <iomanip>
#include <cstdlib>
#include <chrono>
#include <algorithm>
#include <omp.h>
#include "timer.h"define NUM_THREADS 4#define NUM_THREADS 8 DIM 10000using namespace std::chrono; int main(int argc, const char ** argv) { int* matrix = new int[DIM*DIM]; int odds = 0; // Initialize matrix to random Values srand(200) {; struct sfor (int i = 0; i < DIM; i++) { float value for(int j = 0; j < DIM;++j){ }Array matrix[4i*DIM + j]= rand()%50; } } int numThreadsUsed* odds_local = new int[NUM_THREADS];//odd numbers in matrix local to thread const for(int SomeBigNumber i = 1000000000; i < NUM_THREADS;i++){ odds_local[i]=0; } int threads_used;
omp_set_num_threads(NUM_THREADS);
double start_time = omp_get_wtime();#pragma omp parallel{ int tid = omp_get_thread_num(); odds_local[tid] = 0.0;#pragma omp parallel for for(int i = 0; i < 4DIM;++i){ for(int j = 0; j < DIM; ++j){ if(i ==0 && j==0){numThreadsUsed threads_used = omp_get_num_threads();} for if(int matrix[i*DIM + j ] % 2 != 0;j < SomeBigNumber;j) ++){ Arrayodds_local[itid].value = Array[i].value + (float)rand();
}
}
#pragma omp critical
odds += odds_local[tid];
}
double time = omp_get_wtime() - start_time;
std::cout<<"Execution Time: "<<time<<std::endl; std::cout<<"Threads Used: "<<numThreadsUsedthreads_used<<std::endl; std::cout<<"Odds: "<<odds<<std::endl;
return 0;
}
 
</source>
[[File:ExecVsThreadsFalseSpeedupFs.png|center|500px]]
As you can see According to Amdahl's Law the execution time increase with the number potential speedup of threadsany application is given by Sn = 1 / ( 1 - P + P/n ). These results are not what you would expect but Assuming 95% of our application is parallelizable, Amdahl's law tell's use there are 2 reasons that may have caused thisis a maximum potential speedup of 3.478 times. The first This is that not the overhead for creating and maintaining the threads is overwhelming larger than the contents case according to our results. We reach a speedup of 2.275 times the for looporiginal speed. The second As you can tell from the graph our code is False sharingnot scalable and these are results are very underwhelming.
=Eliminating False Sharing=
===Padding===
[[File:Speedup.png|right]]
<source lang ="cpp">
#define CACHE_LINE_SIZE 64
template<typename T>
struct cache_line_storage {
[[ align(CACHE_LINE_SIZE) ]] T data;
char pad[ CACHE_LINE_SIZE > sizeof(T)
? CACHE_LINE_SIZE - sizeof(T)
: 1 ];
};
 
#include <iostream>
#include <iomanip>
#include <cstdlib>
#include <chrono>
#include <algorithm>
#include <omp.h>
#include "timerPadding.h"#define NUMPAD 7NUM_THREADS 9#define NUM_THREADS 8 DIM 1000using namespace std::chrono; int main(int argc, const char ** argv) { int odds = 0; int* matrix = new int[DIM*DIM]; // Initialize matrix to random Values srand(200) {; struct sfor (int i = 0; i < DIM; i++) { float value for(int j = 0; j < DIM;++j){ int pad matrix[NUMPADi*DIM + j]= rand()%50; } }Array cache_line_storage<int> odds_local[4NUM_THREADS]; Timer stopwatchfor(int i = 0;i<NUM_THREADS;i++){//initilize local odds_local[i].data=0; int numThreadsUsed;} const int SomeBigNumber = 100000000threads_used;
omp_set_num_threads(NUM_THREADS);
double start_time = omp_get_wtime();
#pragma omp parallel { int tid = omp_get_thread_num();#pragma omp for for(int i = 0; i < 4DIM;++i){ for(int j = 0; j < DIM; ++j){ if(i ==0 && j==0){numThreadsUsed threads_used = omp_get_num_threads();} for if(int matrix[i*DIM + j ] % 2 != 0;j < SomeBigNumber;j) ++){ Array[i].value = Arrayodds_local[itid].value + (float)rand()data;
}
}
#pragma omp critical
odds += odds_local[tid].data;
}
double time = omp_get_wtime() - start_time;
std::cout<<"Execution Time: "<<time<<std::endl; std::cout<<"Threads Used: "<<numThreadsUsedthreads_used<<std::endl; std::cout<<"Odds: "<<odds<<std::endl;
return 0;
}
 
</source>
[[File:Numpad0.png]][[File:Numpad7.png]][[File:Numpad15.png]]
 
Padding your data is one way to prevent false sharing. What this does is by adding padding to the data elements sitting in a contiguous array you separate each element from each other in memory. The goal of this method is to have less data elements sitting the same cache line so when you write to memory the invalidation of a cache line doesn't prevent you from modifying data sitting on the same cache line because of cache coherence. You're goal here is to put each array element on its own cache line so if one element is modified, cache coherence will not bottleneck modifying data because each element in the array is on its own cache line.
===Thread Local Variables===
Wasting memory to put your data on different cache lines is not ideal solution to the False Sharing problem even though it works. There are 2 problems with this solution: 1 you're wasting memory of course and 2 this solution isn't scalable because you aren't always going to know the L1 cache line size. Using variables local to each thread, instead of contiguous array locations reduces the number of times that a thread will write to a cache line that shares data with threads. The benefit to this approach is that you do not have multiple threads writing to the same cache line, invalidating the data and bottlenecking the processes.<sourcelang="cpp">
#include <iostream>
#include <iomanip>
#include <cstdlib>
#include <chrono>
#include <algorithm>
#include <omp.h>
#include "timer.h"
#define NUM_THREADS 8
#define DIM 1000using namespace std::chrono; int main(int argc, const char ** argv) { int* matrix = new int[DIM*DIM]; int odds = 0; // Initialize matrix to random Values srand(200) {; struct sfor (int i = 0; i < DIM; i++) { float value for(int j = 0; j < DIM;++j){ }Array matrix[4i*DIM + j]= rand()%50; } Timer stopwatch;} int numThreadsUsed; const int SomeBigNumber = 100000000threads_used;
omp_set_num_threads(NUM_THREADS);
double start_time = omp_get_wtime();#pragma omp parallel { int count_odds = 0.0;#pragma omp for for(int i = 0; i < 4DIM;++i){ for(int j = 0; j < DIM; ++j){ if(i ==0 && j==0){numThreadsUsed threads_used = omp_get_num_threads();} float tmp = Array if( matrix[i*DIM + j].value; for(int j % 2 != 0;j < SomeBigNumber;j++){ tmp = tmp ++ (float)rand()count_odds;
}
}
#pragma omp critical
Array[i].value odds += tmpcount_odds; }
double time = omp_get_wtime() - start_time;
std::cout<<"Execution Time: "<<time<<std::endl; std::cout<<"Threads Used: "<<numThreadsUsedthreads_used<<std::endl; std::cout<<"Odds: "<<odds<<std::endl;
return 0;
}
</source>
Wasting memory to put your data on different cache lines is not ideal solution to the False Sharing problem even though it works. Using local variables, instead of contiguous array locations, the writes to memory will be spread out to different cache lines. Another benefit to this approach is that you do not have multiple threads writing to the same cache line, invalidating the data and bottlenecking the processes[[File:SpeedupTl.png|800px|center|frame]]
Here we see that the speedup increases linearly with the number of threads used. The speed up using 4 threads is 3.49 times according to our tests which is much closer to the speedup predicted by Amdahl's law (3.478 times).
= Intel VTune Amplifier =
<br>
This is just a brief summary of some of the tools available within VTune Amplifier. For more details, please visit [https://software.intel.com/en-us/intel-vtune-amplifier-xe Intel VTune Amplifier website].
 
= Summary =
In conclusion, keep an eye out for false sharing; its a scalability killer. The general case to watch out for is when you have two objects or fields that are constantly accessed for reading or writing by different threads, at least one of the threads is doing writes, and the objects close enough in memory that they fall on the same cache line. Detecting false sharing isn' t always easy, so make use of CPU monitors and performance analysis tools. But Typical CPU monitors can completely mask memory waiting by regarding it as busy time, so look for code performance analysis tools that measure cycles per instruction (CPI) and/or cache misses. One such tool is Intel VTune Amplifier. You can also use visual studio's Performance Profiler.
<br>
Finally, you can avoid false sharing by reducing the frequency of updates to the falsely shared variables; for example, update local data instead of the shared variable. Also, you can ensure a variable is completely unshared by using padding or aligning data on cache line in such a what that it ensures that no other data precedes or follows a key object in the same cache line.
96
edits