【发布时间】:2016-03-30 16:46:24
【问题描述】:
我实现了strlen()函数的不同方式,包括SSE2 assembly、SSE4.2 assembly和SSE2 intrinsic,我也对它们进行了一些实验,strlen() in <string.h>和strlen() in glibc。但是,它们在毫秒(时间)方面的表现是出乎意料的。
我的实验环境:
CentOS 7.0 + gcc 4.8.5 + Intel Xeon
以下是我的实现:
-
strlen使用 SSE2 程序集long strlen_sse2_asm(const char* src){ long result = 0; asm( "movl %1, %%edi\n\t" "movl $-0x10, %%eax\n\t" "pxor %%xmm0, %%xmm0\n\t" "lloop:\n\t" "addl $0x10, %%eax\n\t" "movdqu (%%edi,%%eax), %%xmm1\n\t" "pcmpeqb %%xmm0, %%xmm1\n\t" "pmovmskb %%xmm1, %%ecx\n\t" "test %%ecx, %%ecx\n\t" "jz lloop\n\t" "bsf %%ecx, %%ecx\n\t" "addl %%ecx, %%eax\n\t" "movl %%eax, %0" :"=r"(result) :"r"(src) :"%eax" ); return result; }
2.strlen 使用 SSE4.2 汇编
long strlen_sse4_2_asm(const char* src){
long result = 0;
asm(
"movl %1, %%edi\n\t"
"movl $-0x10, %%eax\n\t"
"pxor %%xmm0, %%xmm0\n\t"
"lloop2:\n\t"
"addl $0x10, %%eax\n\t"
"pcmpistri $0x08,(%%edi, %%eax), %%xmm0\n\t"
"jnz lloop2\n\t"
"add %%ecx, %%eax\n\t"
"movl %%eax, %0"
:"=r"(result)
:"r"(src)
:"%eax"
);
return result;
}
3。 strlen 使用 SSE2 内在
long strlen_sse2_intrin_align(const char* src){
if (src == NULL || *src == '\0'){
return 0;
}
const __m128i zero = _mm_setzero_si128();
const __m128i* ptr = (const __m128i*)src;
if(((size_t)ptr&0xF)!=0){
__m128i xmm = _mm_loadu_si128(ptr);
unsigned int mask = _mm_movemask_epi8(_mm_cmpeq_epi8(xmm,zero));
if(mask!=0){
return (const char*)ptr-src+(size_t)ffs(mask);
}
ptr = (__m128i*)(0x10+(size_t)ptr & ~0xF);
}
for (;;ptr++){
__m128i xmm = _mm_load_si128(ptr);
unsigned int mask = _mm_movemask_epi8(_mm_cmpeq_epi8(xmm,zero));
if (mask!=0)
return (const char*)ptr-src+(size_t)ffs(mask);
}
}
-
我也查了一下linux内核中的实现,下面是它的实现
size_t strlen_inline_asm(const char* str){ int d0; size_t res; asm volatile("repne\n\t" "scasb" :"=c" (res), "=&D" (d0) : "1" (str), "a" (0), "" (0xffffffffu) : "memory"); return ~res-1; }
根据我的经验,我还添加了标准库之一并比较了它们的性能。
以下是我的main功能码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <xmmintrin.h>
#include <x86intrin.h>
#include <emmintrin.h>
#include <time.h>
#include <unistd.h>
#include <sys/time.h>
int main()
{
struct timeval tpstart,tpend;
int i=0;
for(;i<1023;i++){
test_str[i] = 'a';
}
test_str[i]='\0';
gettimeofday(&tpstart,NULL);
for(i=0;i<10000000;i++)
strlen(test_str);
gettimeofday(&tpend,NULL);
printf("strlen from stirng.h--->%lf\n",(tpend.tv_sec-tpstart.tv_sec)*1000+(tpend.tv_usec-tpstart.tv_usec)/1000.0);
gettimeofday(&tpstart,NULL);
for(i=0;i<10000000;i++)
strlen_inline_asm(test_str);
gettimeofday(&tpend,NULL);
printf("strlen_inline_asm--->%lf\n",(tpend.tv_sec-tpstart.tv_sec)*1000+(tpend.tv_usec-tpstart.tv_usec)/1000.0);
gettimeofday(&tpstart,NULL);
for(i=0;i<10000000;i++)
strlen_sse2_asm(test_str);
gettimeofday(&tpend,NULL);
printf("strlen_sse2_asm--->%lf\n",(tpend.tv_sec-tpstart.tv_sec)*1000+(tpend.tv_usec-tpstart.tv_usec)/1000.0);
gettimeofday(&tpstart,NULL);
for(i=0;i<10000000;i++)
strlen_sse4_2_asm(test_str);
gettimeofday(&tpend,NULL);
printf("strlen_sse4_2_asm--->%lf\n",(tpend.tv_sec-tpstart.tv_sec)*1000+(tpend.tv_usec-tpstart.tv_usec)/1000.0);
gettimeofday(&tpstart,NULL);
for(i=0;i<10000000;i++)
strlen_sse2_intrin_align(test_str);
gettimeofday(&tpend,NULL);
printf("strlen_sse2_intrin_align--->%lf\n",(tpend.tv_sec-tpstart.tv_sec)*1000+(tpend.tv_usec-tpstart.tv_usec)/1000.0);
return 0;
}
结果是:(毫秒)
strlen from stirng.h--->23.518000
strlen_inline_asm--->222.311000
strlen_sse2_asm--->782.907000
strlen_sse4_2_asm--->955.960000
strlen_sse2_intrin_align--->3499.586000
我对此有一些疑问:
- 为什么
string.h的strlen这么快?我认为它的代码应该识别为strlen_inline_asm,因为我从/linux-4.2.2/arch/x86/lib/string_32.c复制了代码[http://lxr.oss.org.cn/source/arch/x86/lib/string_32.c#L164] - 为什么
sse2 intrinsic和sse2 assembly在性能上如此不同? - 谁能帮助我如何反汇编代码,以便我可以看到编译器对静态库的函数
strlen进行了哪些转换?我用了gcc -s但是没有找到strlen from the <string.h>的反汇编 - 我认为我的代码可能不太好,如果您能帮助我改进我的代码,尤其是汇编代码,我将不胜感激。
谢谢。
【问题讨论】:
-
我投票决定将此问题作为离题结束,因为它通过在循环中丢弃结果来对标准的纯函数进行基准测试。
-
编译器可能会优化掉
string.hstrlen,因为它知道它会这样做(并且每次结果都是一样的)。您是否检查了测试循环的 asm 输出?编写衡量您实际想要衡量的内容的微基准并非易事。 -
@BecomeBetter:
-O0无法获得有用的基准测试结果。我想知道是否是这种情况,因为内在函数版本的表现有多糟糕。您是否甚至查看过使用-O0从内部函数生成的 asm? -
@BecomeBetter:另外,您应该提及您正在使用的测试字符串的大小 - 如果它们是理智/现实的(例如,对于普通代码来说,最多大约 100 个字符)那么你会得到与在合理代码中从未使用过的“非常大”的字符串截然不同的结果。
-
对这个问题的反对票和反对票是把婴儿和洗澡水一起扔出去。一些基准测试前提可能是错误的,但如果您缺乏批判性检查 SSE 实现的知识 - 您应该继续前进。这个问题有很多值得考虑和平衡的字符串长度等问题。
标签: performance gcc sse inline-assembly intrinsics