任何人都可以给我大概的时间(以纳秒为单位)来访问L1,L2和L3缓存,以及Intel i7处理器上的主内存吗?
虽然这不是一个特别的编程问题,但是对于某些低延迟编程挑战而言,了解这些速度细节是必要的.
我一直试图通过计算一个使用以下代码对数组元素进行扩展和求和的例程来了解在L1缓存与内存中使用数组的影响(我知道我应该将结果缩放为' a'在最后;关键是在循环中同时进行乘法和加法 - 到目前为止,编译器还没有想出要将'a'分解出来):
double sum(double a,double* X,int size)
{
double total = 0.0;
for(int i = 0; i < size; ++i)
{
total += a*X[i];
}
return total;
}
#define KB 1024
int main()
{
//Approximately half the L1 cache size of my machine
int operand_size = (32*KB)/(sizeof(double)*2);
printf("Operand size: %d\n", operand_size);
double* X = new double[operand_size];
fill(X,operand_size);
double seconds = timer();
double result;
int n_iterations = 100000;
for(int i = 0; i < n_iterations; ++i)
{
result = …Run Code Online (Sandbox Code Playgroud) 我听说有英特尔在线书籍描述了特定汇编指令所需的CPU周期,但我无法找到它(经过努力).有人能告诉我如何找到CPU周期吗?
下面是一个例子,在下面的代码中,mov/lock是1个CPU周期,xchg是3个CPU周期.
// This part is Platform dependent!
#ifdef WIN32
inline int CPP_SpinLock::TestAndSet(int* pTargetAddress,
int nValue)
{
__asm
{
mov edx, dword ptr [pTargetAddress]
mov eax, nValue
lock xchg eax, dword ptr [edx]
}
// mov = 1 CPU cycle
// lock = 1 CPU cycle
// xchg = 3 CPU cycles
}
#endif // WIN32
Run Code Online (Sandbox Code Playgroud)
顺便说一句:这是我发布的代码的URL:http://www.codeproject.com/KB/threads/spinlocks.aspx
我们有一个简单的内存吞吐量基准.对于大块内存,它所做的只是重复记忆.
在几台不同的机器上查看结果(针对64位编译),Skylake机器的性能明显优于Broadwell-E,保持OS(Win10-64),处理器速度和RAM速度(DDR4-2133)不变.我们不是说几个百分点,而是大约2个因素.Skylake配置为双通道,Broadwell-E的结果不会因双/三/四通道而异.
任何想法为什么会这样?随后的代码在VS2015的Release中编译,并报告完成每个memcpy的平均时间:
64位:Skylake为2.2ms,Broadwell-E为4.5ms
32位:Skylake为2.2ms,Broadwell-E为3.5ms.
通过利用多个线程,我们可以在四通道Broadwell-E构建上获得更大的内存吞吐量,这很不错,但是看到单线程内存访问的这种巨大差异令人沮丧.为什么差异如此显着的任何想法?
我们还使用了各种基准测试软件,他们验证了这个简单示例所展示的内容 - 单线程内存吞吐量在Skylake上更好.
#include <memory>
#include <Windows.h>
#include <iostream>
//Prevent the memcpy from being optimized out of the for loop
_declspec(noinline) void MemoryCopy(void *destinationMemoryBlock, void *sourceMemoryBlock, size_t size)
{
memcpy(destinationMemoryBlock, sourceMemoryBlock, size);
}
int main()
{
const int SIZE_OF_BLOCKS = 25000000;
const int NUMBER_ITERATIONS = 100;
void* sourceMemoryBlock = malloc(SIZE_OF_BLOCKS);
void* destinationMemoryBlock = malloc(SIZE_OF_BLOCKS);
LARGE_INTEGER Frequency;
QueryPerformanceFrequency(&Frequency);
while (true)
{
LONGLONG total = 0;
LONGLONG max = 0;
LARGE_INTEGER StartingTime, …Run Code Online (Sandbox Code Playgroud) 这三个片段的执行时间:
pageboundary: dq (pageboundary + 8)
...
mov rdx, [rel pageboundary]
.loop:
mov rdx, [rdx - 8]
sub ecx, 1
jnz .loop
Run Code Online (Sandbox Code Playgroud)
还有这个:
pageboundary: dq (pageboundary - 8)
...
mov rdx, [rel pageboundary]
.loop:
mov rdx, [rdx + 8]
sub ecx, 1
jnz .loop
Run Code Online (Sandbox Code Playgroud)
还有这个:
pageboundary: dq (pageboundary - 4096)
...
mov rdx, [rel pageboundary]
.loop:
mov rdx, [rdx + 4096]
sub ecx, 1
jnz .loop
Run Code Online (Sandbox Code Playgroud)
对于第一个片段,在4770K上,每次迭代大约5个周期,对于第二个片段,每次迭代大约9个周期,然后是第三个片段的5个周期.它们都访问完全相同的地址,这是4K对齐的.在第二个片段中,只有地址计算跨越页面边界:rdx并且rdx + 8不属于同一页面,负载仍然是对齐的.如果偏移量很大,则会再次回到5个周期.
这种效果一般如何起作用?
通过ALU指令从加载路由结果,如下所示:
.loop:
mov rdx, …Run Code Online (Sandbox Code Playgroud) 这是我的环境:
$ python
Python 3.8.10 (default, Nov 14 2022, 12:59:47)
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()
$ lscpu | grep "Model name"
Model name: Intel(R) Core(TM) i5-4430 CPU @ 3.00GHz
$ sudo dmidecode --type memory | grep -E "^\\s+(Speed|Type):" -
Type: DDR3
Speed: 1600 MT/s
Type: DDR3
Speed: 1600 MT/s
Run Code Online (Sandbox Code Playgroud)
我在我的机器上始终得到如下计时结果:
$ python -m timeit -s "x = list(range(250)) * 4" "[*x]"
200000 loops, best of 5: 1.54 usec per loop …Run Code Online (Sandbox Code Playgroud) 我希望能够手动预测任意算术的长度(即没有分支或内存,尽管这也很好)x86-64汇编代码将采用特定的体系结构,考虑到指令重新排序,超标量,延迟,消费者价格指数等
什么/描述必须遵循的规则才能实现这一目标?
我想我已经找到了一些初步规则,但是我没有找到任何关于将任何示例代码分解为这个详细程度的引用,所以我不得不做一些猜测.(例如,英特尔优化手册甚至几乎没有提到指令重新排序.)
至少,我正在寻找(1)确认每条规则是正确的,或者是每条规则的正确陈述,以及(2)我可能忘记的任何规则的列表.
addps并且subps使用相同的功能) unit?我如何确定?).和:4此循环已经发出少于超标量宽度(通常)指令的数量.例如,请考虑以下示例代码(计算交叉产品):
shufps xmm3, xmm2, 210
shufps xmm0, xmm1, 201
shufps xmm2, xmm2, 201
mulps xmm0, xmm3
shufps xmm1, xmm1, 210
mulps xmm1, xmm2
subps xmm0, xmm1
Run Code Online (Sandbox Code Playgroud)
我试图预测Haswell的延迟看起来像这样:
; `mulps` Haswell latency=5, CPI=0.5
; `shufps` Haswell latency=1, CPI=1
; `subps` Haswell latency=3, CPI=1
shufps xmm3, xmm2, 210 ; cycle 1
shufps xmm0, xmm1, 201 ; cycle 2
shufps xmm2, xmm2, 201 ; …Run Code Online (Sandbox Code Playgroud) 首先,我在 IvyBridge 上进行了以下设置,我将在注释位置插入测量有效负载代码。前 8 个字节buf存储buf自身的地址,我用它来创建循环携带依赖:
section .bss
align 64
buf: resb 64
section .text
global _start
_start:
mov rcx, 1000000000
mov qword [buf], buf
mov rax, buf
loop:
; I will insert payload here
; as is described below
dec rcx
jne loop
xor rdi, rdi
mov rax, 60
syscall
Run Code Online (Sandbox Code Playgroud)
我插入到有效载荷位置:
mov qword [rax+8], 8
mov rax, [rax]
Run Code Online (Sandbox Code Playgroud)
perf显示循环为 5.4c/iter。有点理解,因为L1d延迟是4个周期。
我颠倒了这两条指令的顺序:
mov rax, [rax]
mov qword [rax+8], 8
Run Code Online (Sandbox Code Playgroud)
结果突然变成9c/iter。我不明白为什么。因为下一次迭代的第一条指令不依赖于当前迭代的第二条指令,所以这个设置应该和 case 1 没有区别。
我也用IACA工具对这两种情况进行静态分析,但是该工具不可靠,因为两种情况预测的结果都是5.71c/iter,与实验相矛盾。 …
x86 assembly micro-optimization microbenchmark micro-architecture
我记得在优化x86速度时通常要避免使用读 - 修改 - 写指令.也就是说,你应该避免类似的东西add [rsi], 10,这会增加存储在其中的内存位置rsi.建议通常是将其拆分为读取 - 修改指令,然后是商店,如下所示:
mov rax, 10
add rax, [rsp]
mov [rsp], rax
Run Code Online (Sandbox Code Playgroud)
或者,您可以使用显式加载和存储以及reg-reg添加操作:
mov rax, [esp]
add rax, 10
mov [rsp], rax
Run Code Online (Sandbox Code Playgroud)
对于现代x86来说,这仍然是合理的建议(并且它曾经是吗?)?1
当然,在内存中的值被多次使用的情况下,RMW是不合适的,因为您将产生冗余的加载和存储.我对只使用一次值的情况感兴趣.
基于对Godbolt的探索,所有icc,clang和gcc都喜欢使用单个RMW指令来编译类似于:
void Foo::f() {
x += 10;
}
Run Code Online (Sandbox Code Playgroud)
成:
Foo::f():
add QWORD PTR [rdi], 10
ret
Run Code Online (Sandbox Code Playgroud)
因此,至少大多数编译器似乎认为RMW很好,当值仅使用一次.
有趣的是,当增量值是全局值而不是成员时,各种编译器不同意,例如:
int global;
void g() {
global += 10;
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,gcc并且clang仍然是单个RMW指令,而icc更倾向于 一个REG-REG具有明确载入和存储地址:
g():
mov eax, DWORD PTR global[rip] …Run Code Online (Sandbox Code Playgroud) 我有一个嵌套的for循环,它生成以下程序集:
# branch target labels manually added for readability
002E20F8 mov ebx,esi
002E20FA mov dword ptr [ebp-10h],3B9ACA00h
002E2101 sub ebx,edi
002E2103 add ebx,7
002E2106 shr ebx,3
002E2109 nop dword ptr [eax]
outer_loop:
002E2110 xor eax,eax
002E2112 xor ecx,ecx
002E2114 cmp edi,esi
002E2116 mov edx,ebx
002E2118 cmova edx,eax
002E211B mov eax,edi
002E211D test edx,edx
002E211F je main+107h (02E2137h) ;end_innerloop
inner_loop:
002E2121 movsd xmm0,mmword ptr [eax]
002E2125 inc ecx ; inc/addsd swapped
002E2126 addsd xmm0,mmword ptr [k]
002E212B add eax,8
002E212E movsd …Run Code Online (Sandbox Code Playgroud) assembly ×7
performance ×5
x86 ×5
c++ ×2
intel ×2
latency ×2
optimization ×2
benchmarking ×1
c ×1
cpu ×1
cpu-cache ×1
cycle ×1
list ×1
low-latency ×1
memory ×1
pipeline ×1
python ×1
superscalar ×1
visual-c++ ×1
x86-64 ×1