其中:
再往后就是内存,内存的后面就是硬盘。
存取速度如下:
建立这么多级的缓存,会引入2个比较重要的问题:
例如:Intel Core i7-8700K ,是一个 6 核的 CPU,每核上的 L1 是 64KB(数据和指令各 32KB),L2 是 256K,L3 有 2MB
CPU每次加载一块数据,这就是 cache line.
主流的 CPU 的 Cache Line 是 64 Bytes(也有的CPU用 32 和 128 Bytes),这就是 CPU 从内存中捞数据上来的最小数据单位。
因为Cache Line是最小单位(64Bytes),所以先把 Cache 分布多个 Cache Line,比如:L1 有 32KB,那么,32KB/64B = 512 个 Cache Line。
如果有一个数据 x 在 CPU 第 0 核的缓存上被更新了,那么其它 CPU 核上对于这个数据 x 的值也要被更新,这就是缓存一致性的问题。(这对上层的代码是透明的)
来看一个程序。在这个程序中,有一个数组,拥有 16M 个元素,每个元素的值都是随机的。我们用这个程序来统计整个数组包含多少个奇数。
程序中总共列了3种方法,并统计了每种方法的执行时间。具体如下:
#include <chrono> #include <iostream> #include <thread> #include <vector> using namespace std; const int total_size = 16*1024*1024; int * test_data = new int [total_size]; const int thread_num = 6; int result[thread_num]; void thread_func_for_multi_thread(int id) { result[id] = 0; int chunk_size = total_size / thread_num + 1; int start = id * chunk_size; int end = min(start + chunk_size, total_size); for (int i=start; i<end; i++) { if (test_data[i] % 2) result[id] ++; } } void thread_func_for_multi_thread_improved(int id) { int count = 0; int chunk_size = total_size / thread_num + 1; int start = id * chunk_size; int end = min(start + chunk_size, total_size); for (int i=start; i<end; i++) { if (test_data[i] % 2) count ++; } result[id] = count; } void thread_func_for_one_thread() { int count = 0; for (int i=0; i<total_size; i++) { if (test_data[i] % 2) count ++; } cout << "One thread: count = " << count << endl; } void test() { // init test_data array for (int i=0; i<total_size; i++) test_data[i] = rand(); for (int i=0; i<thread_num; i++) result[i] = 0; // 方法-1: 多线程处理 vector<thread> vec_thr; auto beg = std::chrono::system_clock::now(); for (int i=0; i<thread_num; i++) { vec_thr.emplace_back(thread_func_for_multi_thread, i); } for (int i=0; i<thread_num; i++) { vec_thr[i].join(); } int sum = 0; for (int i=0; i<thread_num; i++) { sum += result[i]; } cout << "sum = " << sum << endl; auto end = std::chrono::system_clock::now(); auto time_cost = std::chrono::duration_cast<std::chrono::milliseconds>(end - beg).count(); cout << "Elapsed time: " << time_cost << " ms" << endl; // 方法-2: 单线程处理 beg = std::chrono::system_clock::now(); thread one_thrd(thread_func_for_one_thread); one_thrd.join(); end = std::chrono::system_clock::now(); time_cost = std::chrono::duration_cast<std::chrono::milliseconds>(end - beg).count(); cout << "Elapsed time: " << time_cost << " ms" << endl; // 方法-3: 多线程处理,避开 False Sharing vector<thread> vec_thr_2; beg = std::chrono::system_clock::now(); for (int i=0; i<thread_num; i++) { vec_thr_2.emplace_back(thread_func_for_multi_thread_improved, i); } for (int i=0; i<thread_num; i++) { vec_thr_2[i].join(); } sum = 0; for (int i=0; i<thread_num; i++) { sum += result[i]; } cout << "Improved Multi Thread: sum = " << sum << endl; end = std::chrono::system_clock::now(); time_cost = std::chrono::duration_cast<std::chrono::milliseconds>(end - beg).count(); cout << "Elapsed time: " << time_cost << " ms" << endl; } int main() { test(); return 0; }
运行结果如下:
$g++ 2.cpp -lpthread -O3 $./a.out sum = 8387609 Elapsed time: 27 ms One thread: count = 8387609 Elapsed time: 6 ms Improved Multi Thread: sum = 8387609 Elapsed time: 1 ms
由上可见:
方法-1是多线程的方法,耗时30ms;
方法-2是单线程,耗时11ms;
方法-3还是多线程,耗时仅 2ms;
为什么方法-1的多线程比方法-2的单线程还慢呢?
因为 result[] 这个数组中的数据存在同一个 Cache Line 中,而所有的线程都会对这个 Cache Line 进行写操作,从而导致所有的线程都在不断地重新同步 result[] 所在的 Cache Line,因此 12 个线程还跑不过一个线程。这叫 False Sharing
那么方法-3相比方法-1,做的改进是什么呢?
就是用一个本地变量来记录count值,最后结束的时候再赋给 result[i]. 这样,就避免了所有线程不断更新同一个cache line了。
这个程序对于多线程编程中的 Performance 提升还是有相当借鉴意义的。值得记录下来。
参考文献:
CPU Cache
(完)