- 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/
我在想出一个算法时遇到了麻烦... 我有一系列 GPS 数据,以 1 秒为间隔记录时间、速度、距离。假设距离是米,速度是米/秒。可能有超过 2 小时的数据,或 7200 个点。这里的“时间”字段主要是
使用java排序器,即: Collections.sort(myArrayList, new Comparator() { @Override public int c
有什么区别吗 SELECT * FROM my_table 和 SELECT my_column_id FROM my_table 地点: my_table 有百万行 网站上有大量并发用户进行sql查
有2个样本。 在第一个示例中,使用 orderby 可以更快地获得结果。 (根据 phpmyadmin 速度报告) 在另一个例子中,我没有使用 order by,它给出的结果较慢。 (根据 phpmy
我注意到,如果我将训练数据加载到内存中并将其作为 numpy 数组提供到图中,与使用相同大小的 shuffle 批次相比,速度会有很大差异,我的数据有大约 1000 个实例。 使用内存 1000 次迭
我在 python 中使用破折号。我正在绘制记录到 SQLite 数据库中的实时数据,目前,我正在绘制单个值与时间线图。我计划再添加 20 个图表,但目前,随着时间的增加, plotly 变慢,我认为
我试图调用 hasNext Velocity 模板中的方法,以便根据 foreach 循环中的位置影响行为 - 仅 hasNext没有按照文档工作。 这是 Velocity 用户指南的片段,关于 ha
在我正在制作的游戏中,我有两个点,pt1 和 pt2,我想计算出它们之间的角度。我已经在较早的计算中计算出距离。显而易见的方法是对垂直距离上的水平距离进行反正切 (tan(theta) = opp/a
我经常遇到字符串值不存在和/或为空的情况。这是测试这种情况的最佳方法吗? #if( $incentive.disclaimer && $!incentive.disclaimer != '' )
我想将一个模板nested包含在其他模板cont1,cont2和cont3中。 并且嵌套模板应仅对cont1隐藏一个特定控件。 在包含在cont1中之前,我想为一些标志变量$hideMyControl
是否可以更改从“Windows Azure Media Encoder”输出的音频的播放速度? 我正在使用配置为“WMA High Quality Audio”的“Windows Azure Medi
我使用速度将String(template)与字段合并 hi there I'am ${name}, And I'am ${age} old. velocity将字段${name}和${age}与一种
我使用的是 LockedBitmap 类,它简化了 C# 中位图数据的处理。目前它正在将数据复制到本地 byte[] 数组中,然后通过其类方法访问该数组以获取/设置像素颜色值。 这比直接通过指针访问锁
我尝试在 VM_global_library.vm 文件中添加一堆 #set($x=abc) 语句,但这些变量在我的 VM 模板中不可用。 我想为图像的基本路径等设置一个全局变量。这可能吗? 最佳答案
我的项目结构: -src --main ---java ----makers -----SomeClass ---resources ----htmlPattern.vm 如何告诉 SomeClass
我正在尝试从 Velocity 中的字符串中删除不需要的字符(换行符可以,但不能像 EM 和 CAN ASCII 控制字符那样)。 #set($cleanScreen = $cleanScreen.r
我想在日.月.年之间的点处分割日期。例如:2015 年 1 月 14 日至 {14, 01, 2015}这是我使用的代码:dates3.get(0) 包含我从页面的文本字段获取的字符串“14.01.2
之后,从 1.5 升级到速度引擎 1.7 出现了 1.5 没有的问题。为了解释这个问题,我必须展示一个代码片段: #foreach($someVariable in $someCollection)
我想知道从表中选择所有字段是否更快: SELECT * 或只选择您真正需要的: SELECT field1, field2, field3, field4, field5... 假设表有大约 10 个
我正在尝试模仿照片应用程序的行为,在该应用程序中,用户用手指平移照片并且照片具有一定的速度。由于我不会深入的原因,我不能将 UIScrollView 与它的缩放 UIImageView 一起使用,而是
我是一名优秀的程序员,十分优秀!