- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
看一下这段代码:
#include <atomic>
#include <thread>
typedef volatile unsigned char Type;
// typedef std::atomic_uchar Type;
void fn(Type *p) {
for (int i=0; i<500000000; i++) {
(*p)++;
}
}
int main() {
const int N = 4;
std::thread thr[N];
alignas(64) Type buffer[N*64];
for (int i=0; i<N; i++) {
thr[i] = std::thread(&fn, &buffer[i*1]);
}
for (int i=0; i<N; i++) {
thr[i].join();
}
}
N=4
)比单线程版本(
N=1
)慢得多。
N=4
不会慢很多。如果我使用不同的缓存行(将
*1
替换为
*64
),那么
N=4
会变得更快一点:1.1秒。
typedef
),相同的缓存行:
N=4
的情况要慢得多(正如我预期的那样)。如果使用不同的缓存行,则
N=4
与
N=1
具有相似的性能:3.3秒。
N=4
情况下,我没有得到严重的放慢?四个内核在其缓存中具有相同的内存,因此它们必须以某种方式进行同步,不是吗?它们如何才能几乎完美地并行运行?为什么仅原子事件会严重放慢速度?
buffer
。在一个
for
迭代之后(在
fn
中),所有4个内核在其缓存行中都具有
buffer
,但是每个内核写入一个不同的字节。这些高速缓存行如何同步(在非原子情况下)?缓存如何知道哪个字节是脏的?还是有其他机制可以处理这种情况?为什么这种机制比原子一便宜很多(实际上,它几乎是免费的)?
最佳答案
您所看到的基本上是存储缓冲区与store-to-load forwarding组合所产生的效果,尽管共享了缓存行,但每个核心仍可以独立工作。正如我们将在下面看到的,这确实是奇怪的情况,其中更多的争用是不好的,直到某个点,然后更多的争用突然使事情变得非常快!
现在,使用传统的争用 View ,您的代码似乎将具有很高的争用性,因此比理想情况要慢得多。但是,发生的情况是,每个内核在其写缓冲区中获得单个挂起的写操作后,就可以从写缓冲区中满足所有以后的读操作(存储转发),并且即使在写操作完成之后,以后的写操作也才进入缓冲区。核心已失去对缓存行的所有权。这将大部分工作变成了完全本地化的操作。高速缓存行仍在内核之间跳动,但已与内核执行路径脱钩,仅需要不时地实际提交存储。std::atomic
版本根本无法使用此魔术,因为它必须使用lock
ed操作来维护原子性并破坏存储缓冲区,因此您可以看到争用的全部成本和长等待时间原子操作的成本2。
让我们尝试实际收集一些证据,证明这是正在发生的事情。下面的所有讨论都针对基准测试的非atomic
版本,该版本使用volatile
强制对buffer
进行读取和写入。
我们首先检查一下程序集,以确保它符合我们的期望:
0000000000400c00 <fn(unsigned char volatile*)>:
400c00: ba 00 65 cd 1d mov edx,0x1dcd6500
400c05: 0f 1f 00 nop DWORD PTR [rax]
400c08: 0f b6 07 movzx eax,BYTE PTR [rdi]
400c0b: 83 c0 01 add eax,0x1
400c0e: 83 ea 01 sub edx,0x1
400c11: 88 07 mov BYTE PTR [rdi],al
400c13: 75 f3 jne 400c08 <fn(unsigned char volatile*)+0x8>
400c15: f3 c3 repz ret
它很简单:一个五指令循环,加载一个字节,加载的字节递增,一个字节存储,最后循环递增,并有条件地跳回到顶部。在这里,gcc通过破坏
sub
和
jne
,抑制了宏融合而错过了优化,但是总的来说还可以,并且存储转发延迟将在任何情况下限制循环。
perf
进行测量。首先,单线程(
N=1
)情况:
$ perf stat -e task-clock,cycles,instructions,L1-dcache-loads,L1-dcache-load-misses ./cache-line-increment
Performance counter stats for './cache-line-increment':
1070.188749 task-clock (msec) # 0.998 CPUs utilized
2,775,874,257 cycles # 2.594 GHz
2,504,256,018 instructions # 0.90 insn per cycle
501,139,187 L1-dcache-loads # 468.272 M/sec
69,351 L1-dcache-load-misses # 0.01% of all L1-dcache hits
1.072119673 seconds time elapsed
它与我们的预期差不多:L1D丢失几乎为零(占总数的0.01%,可能主要来自循环外的中断和其他代码),命中率刚好超过500,000,000(几乎完全匹配循环迭代的次数)。还要注意,我们可以轻松计算每次迭代的周期:大约5.55。这主要反射(reflect)了存储到装载转发的成本,外加一个周期的增量,这是一个携带的依赖链,因为同一位置被重复更新(并且
volatile
表示无法将其提升到寄存器中)。
N=4
的情况:
$ perf stat -e task-clock,cycles,instructions,L1-dcache-loads,L1-dcache-load-misses ./cache-line-increment
Performance counter stats for './cache-line-increment':
5920.758885 task-clock (msec) # 3.773 CPUs utilized
15,356,014,570 cycles # 2.594 GHz
10,012,249,418 instructions # 0.65 insn per cycle
2,003,487,964 L1-dcache-loads # 338.384 M/sec
61,450,818 L1-dcache-load-misses # 3.07% of all L1-dcache hits
1.569040529 seconds time elapsed
正如预期的那样,L1负载从5亿跃升至20亿,因为有4个线程分别执行5亿负载。 L1D失误的数量也增加了约1000倍,达到约6000万。不过,与20亿个负载(和20亿个商店-未显示,但我们知道它们在那里)相比,这个数字并不多。大约有33个加载项,每个未命中的存储约为33个。这也意味着每个未命中之间有250个循环。
fn()
工作函数,以便线程仍在同一高速缓存行上竞争,但是无法进行存储转发。
x
位置读取然后写入
x + 1
怎么样?我们将为每个线程提供两个连续的位置(即
thr[i] = std::thread(&fn, &buffer[i*2])
),以便每个线程都对两个私有(private)字节进行操作。修改后的
fn()
如下所示:
for (int i=0; i<500000000; i++)
unsigned char temp = p[0];
p[1] = temp + 1;
}
核心循环与之前的循环几乎相同:
400d78: 0f b6 07 movzx eax,BYTE PTR [rdi]
400d7b: 83 c0 01 add eax,0x1
400d7e: 83 ea 01 sub edx,0x1
400d81: 88 47 01 mov BYTE PTR [rdi+0x1],al
400d84: 75 f2 jne 400d78
唯一改变的是我们写入
[rdi+0x1]
而不是
[rdi]
。
load->add->store->load...
依赖性,原始(相同位置)循环实际上也相当缓慢地运行,每次迭代大约需要5.5个周期。这段新代码打破了这一链条!负载不再取决于存储,因此我们几乎可以并行执行所有操作,我希望此循环每次迭代大约运行1.25个周期(5条指令/CPU宽度为4)。
$ perf stat -e task-clock,cycles,instructions,L1-dcache-loads,L1-dcache-load-misses ./cache-line-increment
Performance counter stats for './cache-line-increment':
318.722631 task-clock (msec) # 0.989 CPUs utilized
826,349,333 cycles # 2.593 GHz
2,503,706,989 instructions # 3.03 insn per cycle
500,973,018 L1-dcache-loads # 1571.815 M/sec
63,507 L1-dcache-load-misses # 0.01% of all L1-dcache hits
0.322146774 seconds time elapsed
因此,每次迭代3大约需要1.65个周期,比增加同一位置要快大约3倍。
$ perf stat -e task-clock,cycles,instructions,L1-dcache-loads,L1-dcache-load-misses ./cache-line-increment
Performance counter stats for './cache-line-increment':
22299.699256 task-clock (msec) # 3.469 CPUs utilized
57,834,005,721 cycles # 2.593 GHz
10,038,366,836 instructions # 0.17 insn per cycle
2,011,160,602 L1-dcache-loads # 90.188 M/sec
237,664,926 L1-dcache-load-misses # 11.82% of all L1-dcache hits
6.428730614 seconds time elapsed
因此
比相同位置的情况慢了约4倍。现在,它不仅比单线程情况要慢一点,而且要慢大约20倍。这是您一直在寻找的竞争!现在,L1D丢失的数量也增加了4倍,很好地解释了性能下降,并且与以下想法一致:当存储到负载转发无法隐藏竞争时,丢失的数量会大大增加。SPAN
方法中增加fn()
的连续位置来做到这一点,而不是总是增加相同的位置。例如,如果SPAN
为4,则连续递增4个位置,例如:for (long i=0; i<500000000 / 4; i++) {
p[0]++;
p[1]++;
p[2]++;
p[3]++;
}
请注意,我们仍然总共增加了5亿个位置,只是将增量分散在4个字节中。凭直觉,您会期望整体性能会有所提高,因为您现在拥有长度为SPAN
的1/SPAN
并行依赖项,因此在上述情况下,您可能希望性能提高4倍,因为这4条并行链的运行速度约为总吞吐量的4倍。SPAN
值,这是我们实际获得的时间(以周期为单位):SPAN
的1增加到2和3,这与两种情况下完全并行的情况下的理论值接近。SPAN
,性能就会迅速变差,变平,约为SPAN=1
情况的2.5倍,比SPAN=5
的最佳性能差近10倍。发生的事情是,存储到负载的转发停止发生,因为存储和后续负载在时间/周期上相距足够远,以致于该存储已退休到L1,因此负载实际上必须获得线路并参与MESI。SPAN
值在2到6的范围内(存储转发仍在起作用),则按比例减少了未命中的次数。显然,由于核心循环更快,因此核心能够在每个高速缓存行传输之间“缓冲”更多的存储。void fn(Type *p) {
for (long i=0; i<500000000 / SPAN; i++) {
for (int j = 0; j < SPAN; j++) {
p[j]++;
}
}
}
bash脚本,用于从1到20的所有perf
值运行SPAN
:PERF_ARGS=${1:--x, -r10}
for span in {1..20}; do
g++ -std=c++11 -g -O2 -march=native -DSPAN=$span cache-line-increment.cpp -lpthread -o cache-line-increment
perf stat ${PERF_ARGS} -e cycles,L1-dcache-loads,L1-dcache-load-misses,machine_clears.count,machine_clears.memory_ordering ./cache-line-increment
done
最后,将结果“转置”为适当的CSV:FILE=result1.csv; for metric in cycles L1-dcache-loads L1-dcache-load-misses; do { echo $metric; grep $metric $FILE | cut -f1 -d,; } > ${metric}.tmp; done && paste -d, *.tmp
最终测试int
计数器而不是char
)。如果一切都是原子的,那么您将拥有20亿美元的总和,而在非原子的情况下,总数与该值的接近程度可以粗略地衡量核心绕线通过的频率。如果核心几乎完全在私下工作,则值(value)将接近5亿,而不是20亿,我想这就是您会发现的(值(value)接近5亿)。gcc
仅将所有sub
和jne
融合在一起,则其每次迭代将以1.1个周期运行(仍然比我期望的1.0个周期差)。我将使用-march=haswell
而不是-march=native
做到这一点,但我不会回去更改所有数字。关于c++ - 为什么从多个线程使用相同的缓存行不会导致严重的速度下降?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46919032/
我阅读了有关 JSR 107 缓存 (JCache) 的内容。 我很困惑:据我所知,每个 CPU 都管理其缓存内存(无需操作系统的任何帮助)。 那么,为什么我们需要 Java 缓存处理程序? (如果C
好吧,我是 jQuery 的新手。我一直在这里和那里搞乱一点点并习惯它。我终于明白了(它并不像某些人想象的那么难)。因此,鉴于此链接:http://jqueryui.com/sortable/#dis
我正在使用 Struts 2 和 Hibernate。我有一个简单的表,其中包含一个日期字段,用于存储有关何时发生特定操作的信息。这个日期值显示在我的 jsp 中。 我遇到的问题是hibernate更
我有点不确定这里发生了什么,但是我试图解释正在发生的事情,也许一旦我弄清楚我到底在问什么,就可能写一个更好的问题。 我刚刚安装了Varnish,对于我的请求时间来说似乎很棒。这是一个Magneto 2
解决 Project Euler 的问题后,我在论坛中发现了以下 Haskell 代码: fillRow115 minLength = cache where cache = ((map fill
我正试图找到一种方法来为我网络上的每台计算机缓存或存储某些 python 包。我看过以下解决方案: pypicache但它不再被积极开发,作者推荐 devpi,请参见此处:https://bitbuc
我想到的一个问题是可以从一开始就缓存网络套接字吗?在我的拓扑中,我在通过双 ISP 连接连接到互联网的 HAProxy 服务器后面有 2 个 Apache 服务器(带有 Google PageSpee
我很难说出不同缓存区域 (OS) 之间的区别。我想简要解释一下磁盘\缓冲区\交换\页面缓存。他们住在哪里?它们之间的主要区别是什么? 据我了解,页面缓存是主内存的一部分,用于存储从 I/O 设备获取的
1.题目 请你为最不经常使用(LFU)缓存算法设计并实现数据结构。 实现 LFUCache 类: LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象 in
1.题目 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: ① LRUCache(int capacity) 以正整数作为容量 capacity
我想在访问该 View 时关闭某些页面的缓存。它适用于简单查询模型对象的页面。 好像什么时候 'django.middleware.cache.FetchFromCacheMiddleware', 启
documents为 ExePackage element state Cache属性的目的是 Whether to cache the package. The default is "yes".
我知道 docker 用图层存储每个图像。如果我在一台开发服务器上有多个用户,并且每个人都在运行相同的 Dockerfile,但将镜像存储为 user1_myapp . user2 将其存储为 use
在 Codeigniter 中没有出现缓存问题几年后,我发现了一个问题。我在其他地方看到过该问题,但没有适合我的解决方案。 例如,如果我在 View 中更改一些纯 html 文本并上传新文件并按 F5
我在 Janusgraph 文档中阅读了有关 Janusgraph Cache 的内容。关于事务缓存,我几乎没有怀疑。我在我的应用程序中使用嵌入式 janusgrah 服务器。 如果我只对例如进行读取
我想知道是否有来自终端的任何命令可以用来匹配 Android Studio 中执行文件>使缓存无效/重新启动的使用。 谢谢! 最佳答案 According to a JetBrains employe
我想制作一个 python 装饰器来内存函数。例如,如果 @memoization_decorator def add(a, b, negative=False): print "Com
我经常在 jQuery 事件处理程序中使用 $(this) 并且从不缓存它。如果我愿意的话 var $this = $(this); 并且将使用变量而不是构造函数,我的代码会获得任何显着的额外性能吗?
是的,我要说实话,我不知道varnish vcl,我可以解决一些基本问题,但是我不太清楚,这就是为什么我遇到问题了。 我正在尝试通过http请求设置缓存禁止,但是该请求不能通过DNS而是通过 Varn
在 WP 站点上加载约 4000 个并发用户时遇到此问题。 这是我的配置: F5 负载均衡器 ---> Varnish 4,8 核,32 Gb RAM ---> 9 个后端,4 个核,每个 16 RA
我是一名优秀的程序员,十分优秀!