使用C/Intel程序集,测试128字节内存块是否包含全零的最快方法是什么?

eye*_*ito 15 c optimization performance assembly intel-vtune

继续我的第一个问题,我正在尝试优化通过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;
// ...
Run Code Online (Sandbox Code Playgroud)

相应程序集的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
Run Code Online (Sandbox Code Playgroud)

通过英特尔instrinsics我能够改进:

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;
// ...
Run Code Online (Sandbox Code Playgroud)

相应程序集的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
Run Code Online (Sandbox Code Playgroud)

如您所见,装配指令较少,而且此版本在时序测试中进一步证明更快.

由于我在英特尔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;
Run Code Online (Sandbox Code Playgroud)

它的边界线不可读,并且(不出所料)被证明比上面给出的两个版本慢.我确信必须有一种更优雅的方式来使用_mm_cmpeq_epi64(),并欢迎提供有关如何实现这一目标的建议.

除了使用C语言的内在函数之外,还欢迎使用原始的英特尔汇编语言解决方案来解决这个问题.

amd*_*mdn 14

正如其他人所指出的那样,主要的问题是你正在检查的128字节数据缺少数据缓存和/或TLB并且转向DRAM,这很慢.VTune告诉你这个

cmp    qword ptr [rax],      0x0       0.171s
jnz    0x14000222                     42.426s
Run Code Online (Sandbox Code Playgroud)

你有另一个较小的热点中途

cmp    qword ptr [rax+0x40], 0x0       0.156s
jnz    0x14000222                      2.550s
Run Code Online (Sandbox Code Playgroud)

占用JNZ指令的那些42.4 + 2.5秒实际上是由先前从内存加载引起的停顿...处理器在你描述程序的时间内总共停留45秒......等待DRAM.

你可能会问到第二个热点的中途是什么.好吧,你正在访问128字节,缓存行是64字节,一旦读取了前64个字节,CPU就开始为你预取...但你没有做足够的工作,第一个64字节到完全重叠进入内存的延迟.

Ivy Bridge的内存带宽非常高(这取决于你的系统,但我估计超过10 GB /秒).你的内存块是4GB,如果按顺序访问它,你应该可以在不到1秒的时间内通过它进行压缩,并让CPU为您预先提取数据.

我的猜测是你通过以非连续方式访问128字节块来阻止CPU数据预取器.

将您的访问模式更改为顺序,您会惊讶地发现它的运行速度有多快.然后,您可以担心下一级优化,这将确保分支预测工作正常.

另一件需要考虑的事情是TLB misses.这些都很昂贵,特别是在64位系统中.而不是使用4KB页面考虑使用2MB huge pages.有关以下内容的Windows支持,请参阅此链接:大页面支持(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)很棒的答案!感谢您抽出时间发布链接 - 除OP之外,他们对其他人非常有帮助. (3认同)

Mma*_*rss 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;
Run Code Online (Sandbox Code Playgroud)

不幸的是,我没有64位系统可以编译它,而且我不熟悉编译器对c代码的确切作用,但在我看来,二进制或者比单个更快==比较.我也不知道英特尔内在函数是什么,但可能以与您已经完成的方式类似的方式优化上述代码.
我希望我的回答有所帮助.
Mmarss