继续我的第一个问题,我正在尝试优化通过 VTune 分析 64 位 C 程序发现的内存热点。
特别是,我想找到最快的方法来测试一个 128 字节的内存块是否包含全零。您可以为内存块假设任何所需的内存对齐;我使用了 64 字节对齐。
我正在使用带有 Intel Ivy Bridge Core i7 3770 处理器、32 GB 内存和 Microsoft vs2010 C 编译器的免费版本的 PC。
我的第一次尝试是:
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
我能够通过 Intel 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;
// ...
对应程序集的 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 中的内部函数外,还欢迎针对此问题的原始英特尔汇编语言解决方案。