继续我的第一个问题,我正在尝试优化通过VTune分析64位C程序找到的内存热点。
特别是,我想找到一种最快的方法来测试一个128字节的内存块是否包含全零。您可以假设内存块有任何所需的内存对齐方式;我使用64字节对齐。
我使用的是配备Intel Ivy Bridge Core i7 3770处理器和32 GB内存的PC以及免费版的Microsoft vs2010 C编译器。
我的第一次尝试是:
const char* bytevecM; // 4 GB block of memory, 64-byte aligned
size_t* psz; // size_t is 64-bits
// ...
// "m7 & 0xffffff80" selects the 128 byte block to test for all zeros
psz = (size_t*)&bytevecM[(unsigned int)m7 & 0xffffff80];
if (psz[0] == 0 && psz[1] == 0
&& psz[2] == 0 && psz[3] == 0
&& psz[4] == 0 && psz[5] == 0
&& psz[6] == 0 && psz[7] == 0
&& psz[8] == 0 && psz[9] == 0
&& psz[10] == 0 && psz[11] == 0
&& psz[12] == 0 && psz[13] == 0
&& psz[14] == 0 && psz[15] == 0) continue;
// ...
相应程序集的VTune概要分析如下:
cmp qword ptr [rax], 0x0 0.171s
jnz 0x14000222 42.426s
cmp qword ptr [rax+0x8], 0x0 0.498s
jnz 0x14000222 0.358s
cmp qword ptr [rax+0x10], 0x0 0.124s
jnz 0x14000222 0.031s
cmp qword ptr [rax+0x18], 0x0 0.171s
jnz 0x14000222 0.031s
cmp qword ptr [rax+0x20], 0x0 0.233s
jnz 0x14000222 0.560s
cmp qword ptr [rax+0x28], 0x0 0.498s
jnz 0x14000222 0.358s
cmp qword ptr [rax+0x30], 0x0 0.140s
jnz 0x14000222
cmp qword ptr [rax+0x38], 0x0 0.124s
jnz 0x14000222
cmp qword ptr [rax+0x40], 0x0 0.156s
jnz 0x14000222 2.550s
cmp qword ptr [rax+0x48], 0x0 0.109s
jnz 0x14000222 0.124s
cmp qword ptr [rax+0x50], 0x0 0.078s
jnz 0x14000222 0.016s
cmp qword ptr [rax+0x58], 0x0 0.078s
jnz 0x14000222 0.062s
cmp qword ptr [rax+0x60], 0x0 0.093s
jnz 0x14000222 0.467s
cmp qword ptr [rax+0x68], 0x0 0.047s
jnz 0x14000222 0.016s
cmp qword ptr [rax+0x70], 0x0 0.109s
jnz 0x14000222 0.047s
cmp qword ptr [rax+0x78], 0x0 0.093s
jnz 0x14000222 0.016s
我能够通过英特尔内部设计改进:
const char* bytevecM; // 4 GB block of memory
__m128i* psz; // __m128i is 128-bits
__m128i one = _mm_set1_epi32(0xffffffff); // all bits one
// ...
psz = (__m128i*)&bytevecM[(unsigned int)m7 & 0xffffff80];
if (_mm_testz_si128(psz[0], one) && _mm_testz_si128(psz[1], one)
&& _mm_testz_si128(psz[2], one) && _mm_testz_si128(psz[3], one)
&& _mm_testz_si128(psz[4], one) && _mm_testz_si128(psz[5], one)
&& _mm_testz_si128(psz[6], one) && _mm_testz_si128(psz[7], one)) continue;
// ...
相应程序集的VTune概要分析如下:
movdqa xmm0, xmmword ptr [rax] 0.218s
ptest xmm0, xmm2 35.425s
jnz 0x14000ddd 0.700s
movdqa xmm0, xmmword ptr [rax+0x10] 0.124s
ptest xmm0, xmm2 0.078s
jnz 0x14000ddd 0.218s
movdqa xmm0, xmmword ptr [rax+0x20] 0.155s
ptest xmm0, xmm2 0.498s
jnz 0x14000ddd 0.296s
movdqa xmm0, xmmword ptr [rax+0x30] 0.187s
ptest xmm0, xmm2 0.031s
jnz 0x14000ddd
movdqa xmm0, xmmword ptr [rax+0x40] 0.093s
ptest xmm0, xmm2 2.162s
jnz 0x14000ddd 0.280s
movdqa xmm0, xmmword ptr [rax+0x50] 0.109s
ptest xmm0, xmm2 0.031s
jnz 0x14000ddd 0.124s
movdqa xmm0, xmmword ptr [rax+0x60] 0.109s
ptest xmm0, xmm2 0.404s
jnz 0x14000ddd 0.124s
movdqa xmm0, xmmword ptr [rax+0x70] 0.093s
ptest xmm0, xmm2 0.078s
jnz 0x14000ddd 0.016s
正如您所看到的,装配指令较少,而且此版本在时序测试中被证明更快。
由于我在英特尔SSE / AVX指令方面相当薄弱,我欢迎提供有关如何更好地利用它们来加速此代码的建议。
虽然我搜索了数百种可用的内容,但我可能错过了理想的内容。特别是,我无法有效地使用_mm_cmpeq_epi64();我寻找这种内在的“不相等”版本(这似乎更适合这个问题)但是干涸了。虽然以下代码“有效”:
if (_mm_testz_si128(_mm_andnot_si128(_mm_cmpeq_epi64(psz[7], _mm_andnot_si128(_mm_cmpeq_epi64(psz[6], _mm_andnot_si128(_mm_cmpeq_epi64(psz[5], _mm_andnot_si128(_mm_cmpeq_epi64(psz[4], _mm_andnot_si128(_mm_cmpeq_epi64(psz[3], _mm_andnot_si128(_mm_cmpeq_epi64(psz[2], _mm_andnot_si128(_mm_cmpeq_epi64(psz[1], _mm_andnot_si128(_mm_cmpeq_epi64(psz[0], zero), one)), one)), one)), one)), one)), one)), one)), one), one)) continue;
它的边界线不可读,并且(不出所料)被证明比上面给出的两个版本慢。我确信必须有一种更优雅的方式来使用_mm_cmpeq_epi64(),并欢迎就如何实现这一点提出建议。
除了使用C语言的内在函数之外,还欢迎使用原始的英特尔汇编语言解决方案来解决这个问题。
答案 0 :(得分:14)
正如其他人所指出的那样,主要问题是你正在检查的128字节数据缺少数据缓存和/或TLB并且转向DRAM,这很慢。 VTune告诉你这个
cmp qword ptr [rax], 0x0 0.171s
jnz 0x14000222 42.426s
你有另一个较小的热点中途
cmp qword ptr [rax+0x40], 0x0 0.156s
jnz 0x14000222 2.550s
那些42.4 + 2.5秒的JNZ指令实际上是由以前的内存负载引起的失速...处理器在你描述程序的时候总是在45秒内无所事事......等待DRAM。
你可能会问第二个热点的中途是什么。好吧,你正在访问128字节,缓存行是64字节,一旦读取了前64个字节,CPU就开始为你预取...但你没有做足够的工作,第一个64字节到完全重叠进入记忆的延迟。
Ivy Bridge的内存带宽非常高(这取决于你的系统,但我估计超过10 GB /秒)。你的内存块是4GB,你应该能够在不到1秒的时间内通过顺序访问它并让CPU预先为你预取数据。
我猜你是通过以非连续方式访问128字节块来阻止CPU数据预取器。
将您的访问模式更改为顺序,您会惊讶地发现它的运行速度有多快。 然后,您可以担心下一级优化,这将确保分支预测正常运行。
要考虑的另一件事是TLB misses
。这些都很昂贵,特别是在64位系统中。而不是使用4KB页面考虑使用2MB huge pages
。有关这些内容的支持,请参阅此链接:Large-Page Support (Windows)
如果您必须以某种随机的方式访问4GB数据,但事先知道m7
值的序列(您的索引),那么您可以pipeline
在您的前面明确获取内存使用(当你使用它时,需要几百个CPU周期)。参见
以下是一些可能对内存优化主题有帮助的链接
Ulrich Drepper每位程序员应该了解的记忆
http://www.akkadia.org/drepper/cpumemory.pdf
机器架构:Herb Sutter编程语言永远不会告诉你的事情
http://www.gotw.ca/publications/concurrency-ddj.htm
http://nwcpp.org/static/talks/2007/Machine_Architecture_-_NWCPP.pdf
http://video.google.com/videoplay?docid=-4714369049736584770#
答案 1 :(得分:5)
对于答案帖我很抱歉,我没有足够的评论声誉 如果您使用以下内容作为测试会发生什么?
if( (psz[0] | psz[1] | psz[2] | psz[3] |
psz[4] | psz[5] | psz[6] | psz[7] |
psz[8] | psz[9] | psz[10] | psz[11] |
psz[12] | psz[13] | psz[14] | psz[15] ) == 0) continue;
不幸的是,我没有64位系统可以编译它,我不熟悉编译器使用c代码做了什么,但在我看来,二进制文件或者比单个文件更快==比较。我也不知道英特尔内在函数是什么,但也可能以与您已经完成的方式类似的方式优化上述代码。
我希望我的回答有所帮助
Mmarss
答案 2 :(得分:2)
98%的128字节块全部为零,每平均4K页面平均少于一个非零字节。对于稀疏的数组,您是否尝试将其存储为稀疏数组?您将节省大量内存和随之而来的缓存未命中延迟;如果一个普通的std :: map变得更快,我不会感到惊讶。
答案 3 :(得分:2)
您是否考虑过英特尔字符串扫描说明?这些往往具有非常高的数据速率,并且处理器知道数据访问是连续的。
mov rdi, <blockaddress>
cld
xor rax, rax
mov rcx, 128/8
repe scasq
jne ...
这无助于您的数据不在缓存中的问题。如果你知道要提前考虑哪个块,你可以通过使用英特尔的预取指令来解决这个问题。 见http://software.intel.com/en-us/articles/use-software-data-prefetch-on-32-bit-intel-architecture
[EDITS编写代码以整理评论中指出的轻微打嗝]
答案 4 :(得分:1)
感谢您收到的优秀提示。
我确信Mmarss“mega or”方法会提高性能,因为它产生的汇编语言指令更少。然而,当我运行我的基准程序时,我花了163秒而不是150秒,而我原来的笨重&amp;&amp;解决方案和我原来笨重的英特尔instrinsics解决方案的145秒(这两个在我的原始帖子中描述)。
为了完整性,这里是我用于“mega或”方法的C代码:
if ((psz[0] | psz[1] | psz[2] | psz[3]
| psz[4] | psz[5] | psz[6] | psz[7]
| psz[8] | psz[9] | psz[10] | psz[11]
| psz[12] | psz[13] | psz[14] | psz[15]) == 0) continue;
VTune集会是:
mov rax, qword ptr [rcx+0x78] 0.155s
or rax, qword ptr [rcx+0x70] 80.972s
or rax, qword ptr [rcx+0x68] 1.292s
or rax, qword ptr [rcx+0x60] 0.311s
or rax, qword ptr [rcx+0x58] 0.249s
or rax, qword ptr [rcx+0x50] 1.229s
or rax, qword ptr [rcx+0x48] 0.187s
or rax, qword ptr [rcx+0x40] 0.233s
or rax, qword ptr [rcx+0x38] 0.218s
or rax, qword ptr [rcx+0x30] 1.742s
or rax, qword ptr [rcx+0x28] 0.529s
or rax, qword ptr [rcx+0x20] 0.233s
or rax, qword ptr [rcx+0x18] 0.187s
or rax, qword ptr [rcx+0x10] 1.244s
or rax, qword ptr [rcx+0x8] 0.155s
or rax, qword ptr [rcx] 0.124s
jz 0x1400070b9 0.342s
然后我尝试通过以下方式将“超级或”想法转换为英特尔教义:
__m128i tt7;
// ...
tt7 = _mm_or_si128(_mm_or_si128(_mm_or_si128(psz[0], psz[1]),
_mm_or_si128(psz[2], psz[3])),
_mm_or_si128(_mm_or_si128(psz[4], psz[5]),
_mm_or_si128(psz[6], psz[7])));
if ( (tt7.m128i_i64[0] | tt7.m128i_i64[1]) == 0) continue;
虽然结果也慢了,耗时155秒。它的VTune集会是:
movdqa xmm2, xmmword ptr [rax] 0.047s
movdqa xmm0, xmmword ptr [rax+0x20] 75.461s
movdqa xmm1, xmmword ptr [rax+0x40] 2.567s
por xmm0, xmmword ptr [rax+0x30] 1.867s
por xmm2, xmmword ptr [rax+0x10] 0.078s
por xmm1, xmmword ptr [rax+0x50] 0.047s
por xmm2, xmm0 0.684s
movdqa xmm0, xmmword ptr [rax+0x60] 0.093s
por xmm0, xmmword ptr [rax+0x70] 1.214s
por xmm1, xmm0 0.420s
por xmm2, xmm1 0.109s
movdqa xmmword ptr tt7$[rsp], xmm2 0.140s
mov rax, qword ptr [rsp+0x28] 0.233s
or rax, qword ptr [rsp+0x20] 1.027s
jz 0x1400070e2 0.498s
上面的英特尔内部解决方案非常粗糙。欢迎提出改进建议。
这再次显示了衡量的重要性。几乎每当我猜到哪个会更快我就错了。也就是说,只要你仔细衡量每一个变化,就不会变得更糟,只能改善。虽然我经常倒退(如上所述),但在过去的一周里,我已经能够将小测试程序的运行时间从221秒减少到145秒。鉴于真正的程序将会运行几个月,这将节省几天。
答案 5 :(得分:0)
建议:将数组对齐到128B,因此空间预取器总是希望填充正确的缓存行以生成128B对缓存行。 Intel optimization manual,第2-30页(PDF格式第60页),描述Sandybridge / Ivybridge:
Spatial Prefetcher:此预取程序努力完成提取到L2缓存的每个缓存行 成对线将其完成为128字节对齐的块。
当您的阵列仅与64B对齐时,读取128B可以触摸两对缓存行,导致L2空间预取器为您可能永远不会使用的数据发出更多负载。
你的回答是正确的想法:或者将块与向量一起,然后测试全零。使用单个分支可能比每8个字节单独分支更好。
但是你测试矢量的策略很糟糕:不要存储它,然后标量加载+ OR两半。这是SSE4 PTEST的完美用例,可让我们避免使用the usual pcmpeqb / pmovmskb
:
ptest xmm0,xmm0 ; 2 uops, and Agner Fog lists it as 1c latency for SnB/IvB, but this is probably bogus. 2c is more likely
jz vector_is_all_zero
; 3 uops, but shorter latency and smaller code-size than pmovmskb
通常分支预测良好,生成其标记输入的延迟并不重要。但在这种情况下,主要的瓶颈是分支错误预测。因此,花费更多的uops(如果需要)以减少延迟可能是值得的。
我不确定在加载第二个缓存行之前测试第一个缓存行是否更好,以防您发现非零字节而不会遇到第二个缓存未命中。空间预取器无法立即加载第二个缓存行,因此可能在加载第二个64B缓存行之前尝试提前退出,除非这会导致许多额外的分支误预测。
所以我可能会这样做:
allzero_128B(const char *buf)
{
const __m128i *buf128 = (const __m128i*)buf; // dereferencing produces 128b aligned-load instructions
__m128i or0 = _mm_or_si128(buf[0], buf[2]);
__m128i or2 = _mm_or_si128(buf[1], buf[3]);
__m128i first64 = _mm_or_si128(or0, or2);
// A chain of load + 3 OR instructions would be fewer fused-domain uops
// than load+or, load+or, or(xmm,xmm). But resolving the branch faster is probably the most important thing.
if (_mm_testz_si128(first64, first64)
return 0;
__m128i or4 = _mm_or_si128(buf[4], buf[6]);
__m128i or6 = _mm_or_si128(buf[5], buf[7]);
__m128i first64 = _mm_or_si128(or4, or6);
}
在IvyBrige上,使用256b AVX操作可以获得多少收益。 Vector-FP 256b VORPS ymm每uop的工作量是原来的两倍,但只能在port5上运行。 (POR xmm在p015上运行)。 256b负载作为两个128b的一半完成,但它们仍然只有1个uop。
我没有看到使用单个CMPEQPS检查256b向量为全零的方法。 +0.0比较等于-0.0,因此符号位位置的1位在与零的比较中不会被检测到。我也不认为任何CMPPS谓词都有帮助,因为它们都没有实现比较+0.0与+0.0不同的比较。 (有关FP等式的更多信息,请参阅SIMD instructions for floating point equality comparison (with NaN == NaN)。)
; First 32B arrives in L1D (and load buffers) on cycle n
vmovaps ymm0, [rdi+64] ; ready on cycle n+1 (256b loads take 2 cycles)
vorps ymm0, ymm0, [rdi+96] ; ready on cycle n+3 (the load uop is executing on cycles n+1 and n+2)
vextractf128 xmm1, ymm0, 1 ; 2c latency on IvB, 3c on Haswell
; xmm1 ready on cycle n+5
vpor xmm0, xmm0, xmm1 ; ready on n+6 (should be no bypass delay for a shuffle (vextractf128) -> integer booleans)
vptest xmm0, xmm0
jz second_cacheline_all_zero
不,那不比
好; First 32B of the cache-line arrives in L1D on cycle n (IvB has a 32B data path from L2->L1)
vmovaps xmm0, [rdi+64] ; result ready on cycle n
vmovaps xmm1, [rdi+64 + 16] ; result ready on cycle n (data should be forwarded to outstanding load buffers, I think?)
vpor xmm0, xmm0, [rdi+64 + 32] ; ready on cycle n+1
vpor xmm1, xmm1, [rdi+64 + 48] ; ready on cycle n+1, assuming the load uops get their data the cycle after the first pair.
vpor xmm0, xmm1 ; ready on cycle n+2
vptest xmm0, xmm0
jz second_cacheline_all_zero
对于AVX2,256b操作是有意义的,包括VPTEST ymm,ymm。