为什么SSE4.2 cmpstr比常规代码慢?

我正在尝试验证仅包含ASCII可见字符,空格和\ t的字符串。

但是,在大多数CPU上,ASCII表查找似乎比带有_SIDD_CMP_RANGES的_mm_cmpestri指令快。 我已经在i5-2410M,i7-3720QM,i7-5600U和KVM虚拟化Xeon未知类型上进行了测试,只有在最后一个版本上,矢量化版本才更快。

我的测试代码在这里:

(
    [09:00] => stdClass Object
        (
            [duration] => 40
        )

    [10:00] => stdClass Object
        (
            [duration] => 60
        )

    [11:15] => 
    [11:30] => 
    [11:45] => stdClass Object
        (
            [duration] => 45
        )

)

使用#include <stdio.h> #include <string.h> #include <inttypes.h> #include <sys/time.h> #include <sys/mman.h> #include <immintrin.h> #include <stdalign.h> #include <stdlib.h> #define MIN(a,b) (((a)<(b))?(a):(b)) #define ALIGNED16 alignas(16) #define MEASURE(msg,stmt) { \ struct timeval tv; \ gettimeofday(&tv,NULL); \ uint64_t us1 = tv.tv_sec * (uint64_t)1000000 + tv.tv_usec; \ stmt; \ gettimeofday(&tv,NULL); \ uint64_t us2 = tv.tv_sec * (uint64_t)1000000 + tv.tv_usec; \ printf("%-20s - %.4fms\n",msg,((double)us2 - us1) / 1000); \ } // Character table #define VWSCHAR(c) (vis_ws_chars[(unsigned char)(c)]) // Visible characters and white space #define YES 1,#define NO 0,#define YES16 YES YES YES YES YES YES YES YES YES YES YES YES YES YES YES YES #define NO16 NO NO NO NO NO NO NO NO NO NO NO NO NO NO NO NO #define NO128 NO16 NO16 NO16 NO16 NO16 NO16 NO16 NO16 // Visible ASCII characters with space and tab ALIGNED16 static const int vis_ws_chars[256] = { // NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI NO NO NO NO NO NO NO NO NO YES NO NO NO NO NO NO // DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US NO16 // SP ! " # $ % & ' ( ) * +,- . / // 0 1 2 3 4 5 6 7 8 9 : ; < = > ? // @ A B C D E F G H I J K L M N O // P Q R S T U V W X Y Z [ \ ] ^ _ // ` a b c d e f g h i j k l m n o YES16 YES16 YES16 YES16 YES16 // p q r s t u v w x y z { | } ~ DEL YES YES YES YES YES YES YES YES YES YES YES YES YES YES YES NO // Non-ASCII characters NO128 }; size_t search_logic(const char* data,size_t len) { __m128i ht = _mm_set1_epi8('\t'); //__m128i del = _mm_set1_epi8(0x7f); __m128i td = _mm_set1_epi8('~'); __m128i sp_m1 = _mm_set1_epi8(' ' - 1); size_t i = 0; while (len - i >= 16) { __m128i c = _mm_loadu_si128((const __m128i *) (data + i)); // (!((c < del) && (c >= sp)) && (c != ht)) == 0 //if(!_mm_testc_si128(_mm_and_si128(_mm_cmpgt_epi8(c,sp_m1),_mm_cmplt_epi8(c,del)),_mm_xor_si128(c,ht))) //break; // !(c == del) && ((c == ht) || (c >= sp)) == 1 //if(!_mm_test_all_ones(_mm_andnot_si128(_mm_cmpeq_epi8(c,del),_mm_or_si128(_mm_cmpeq_epi8(c,ht),_mm_cmpgt_epi8(c,sp_m1))))) //break; // (((c != ht) && (c >= sp)) && (c > td)) == 0 if(!_mm_test_all_zeros(_mm_and_si128(_mm_xor_si128(c,sp_m1)),td))) break; i += 16; } // Check last 15 bytes for (; i < len; ++i) { if (!VWSCHAR(data[i])) { break; } } return i; } size_t search_table(const char* data,size_t len) { // Search non-matching character via table lookups size_t i = 0; while (len - i >= 16) { if (!VWSCHAR(data[i + 0])) break; if (!VWSCHAR(data[i + 1])) break; if (!VWSCHAR(data[i + 2])) break; if (!VWSCHAR(data[i + 3])) break; if (!VWSCHAR(data[i + 4])) break; if (!VWSCHAR(data[i + 5])) break; if (!VWSCHAR(data[i + 6])) break; if (!VWSCHAR(data[i + 7])) break; if (!VWSCHAR(data[i + 8])) break; if (!VWSCHAR(data[i + 9])) break; if (!VWSCHAR(data[i + 10])) break; if (!VWSCHAR(data[i + 11])) break; if (!VWSCHAR(data[i + 12])) break; if (!VWSCHAR(data[i + 13])) break; if (!VWSCHAR(data[i + 14])) break; if (!VWSCHAR(data[i + 15])) break; i += 16; } // Check last 15 bytes for (; i < len; ++i) { if (!VWSCHAR(data[i])) { break; } } return i; } size_t search_sse4cmpstr(const char* data,size_t len) { static const char legal_ranges[16] = { '\t','\t',' ','~',}; __m128i v1 = _mm_loadu_si128((const __m128i*)legal_ranges); size_t i = 0; while (len - i >= 16) { __m128i v2 = _mm_loadu_si128((const __m128i*)(data + i)); unsigned consumed = _mm_cmpestri(v1,4,v2,16,_SIDD_LEAST_SIGNIFICANT|_SIDD_CMP_RANGES|_SIDD_UBYTE_OPS|_SIDD_NEGATIVE_POLARITY); i += consumed; if (consumed < 16) { return i; } } // Check last 15 bytes for (; i < len; ++i) { if (!VWSCHAR(data[i])) { break; } } return i; } size_t search_sse4cmpstr_implicit(const char* data,}; __m128i v1 = _mm_loadu_si128((const __m128i*)legal_ranges); size_t i = 0; while (len - i >= 16) { __m128i v2 = _mm_loadu_si128((const __m128i*)(data + i)); unsigned consumed = _mm_cmpistri(v1,_SIDD_LEAST_SIGNIFICANT|_SIDD_CMP_RANGES|_SIDD_UBYTE_OPS|_SIDD_NEGATIVE_POLARITY); i += consumed; if (consumed < 16) { return i; } } // Check last 15 bytes for (; i < len; ++i) { if (!VWSCHAR(data[i])) { break; } } return i; } int main() { printf("Setting up 1GB of data...\n"); size_t len = 1024 * 1024 * 1024 + 3; char* data = (char*)mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE,-1,0); // Aligned srand(0); for (size_t i = 0; i < len; ++i) { const char v = rand() % 96; data[i] = v == 95 ? '\t' : ' ' + v; } size_t end = len - 2; data[end] = '\n'; // Illegal character to be found MEASURE("table lookup",{ size_t i = search_table(data,len); if (i != end) printf("INCORRECT RESULT: %zu instead of %zu",i,end); }); MEASURE("cmpestr ranges",{ size_t i = search_sse4cmpstr(data,end); }); MEASURE("cmpistr ranges",{ size_t i = search_sse4cmpstr_implicit(data,end); }); MEASURE("logic ranges",{ size_t i = search_logic(data,end); }); } 进行编译,可以得到以下结果:

gcc -O3 -march=native -pedantic -Wall -Wextra main2.cpp

我还检查了汇编输出,当search_table未向量化时,search_sse4cmpstr使用vpcmpestri。

我使用错了吗?还是为什么这条指令根本不存在?

编辑: 正如评论中指出的那样,cmpistr(带有较少参数的隐式长度指令)比cmpestr稍快,有时甚至比表查找快。

但是,SSE2的按位和整数运算似乎更快。

EDIT2 彼得·科德斯(Peter Cordes)找到了正确的答案。 我已经在新答案中添加了修改后的程序,因此,如果您对cmpstr感兴趣,请查看此程序。

请勿使用上面的代码!

yqf1996 回答:为什么SSE4.2 cmpstr比常规代码慢?

该代码对先前的向量有i的不必要依赖,成为pcmpestri的瓶颈+大约12 + 5个周期的L1d负载使用延迟。({{ 3}}和https://agner.org/optimize/),是的,很遗憾,您使用的是错误的。

如果您编写的代码与标量循环类似,则执行i+=16并仅将pcmpestri结果作为循环退出条件进行检查,那么您的瓶颈将成为吞吐量 Sandybridge系列CPU上每4个时钟1个向量的数量。 (特别是SnB和IvB)。

或者,如果您的输入可以使用pcmpistri,则情况会好一些,并且可以在Sandybridge系列上以每3个时钟1个的频率运行。

起初我没有注意到这个问题,因为我不希望这样写循环,而且asm循环中还存在其他混乱情况。 ://我花了很多时间对perf进行分析,以确保它不是Skylake CPU上的微码(8 uop)指令的前端瓶颈。请参阅现在存档的注释。

吞吐量瓶颈使您每循环大约需要4个字节,而 另一种方式大约为1(每个输入字节2个负载,而Intel因为SnB每个时钟可以进行2个负载)。因此,速度提高了4倍。或Nehalem的8倍,负载吞吐量为1 /时钟。

延迟巧合,每个输入字节大约只有1个周期,与表查找大致相同。


此外,请勿使用len - i < 16; gcc实际上计算出循环内部会产生额外的成本。一旦知道i < len-15,就使用len>=16。 (无符号类型之所以棘手,是因为它们以零换行;您希望将其编译为cmp / jcc来跳过循环,然后是do{}while asm循环结构。因此,最初的len>=16实际上是与正常的循环条件分开。)


关于pcmpestri 的其他有趣事实:

  • https://uops.info/(速度较慢,尤其是对于AVX2)
  • How much faster are SSE4.2 string instructions than SSE2 for memcmp?是的,显式长度版本比隐式长度版本慢。显然,与扫描现有输入中的0字节相比,基于额外的2个长度的输入进行屏蔽会更慢,并且成本也更高。
  • 性能不取决于即时值。我一下子认为确实如此,但这取决于结果i,因此更改立即数会导致高速缓存行拆分,从而使循环延迟变得更糟。使用i+=16循环进行重新测试没有效果。
  • 如果与REX.W前缀一起使用(以RAX和RDX代替EAX和EDX接受输入),对于Intel来说它要慢得多(根据SSE42 & STTNI - PcmpEstrM is twice slower than PcmpIstrM,is it true?),但是没有内在的含义,因此您不必不必担心编译器会这样做。
  

或者为什么这条指令根本存在?

这些说明在Nehalem中介绍。如果它们“流行”并被广泛使用,例如,英特尔可能有计划使其更快。用于短字符串strcmp。但是如果没有错误抑制(对于可能会跨入新页面的未对齐负载),如果不检查指针内容就很难使用它们。如果仍然要进行检查,则最好使用有效的pcmpeqb / pmovmskb,它的操作量更少。也许可以用pminub / pcmpeqb / pmovmskb-> bsf在任一字符串中找到第一个零。也许有一个strcmp初始启动的SSE4.2用例,但是一旦开始使用就没那么多了。

世界上大多数人都关心UTF-8,而不是8位字符集。而且由于UTF-16不再是固定宽度(由于使用32位Unicode),因此即使是宽字符的东西也很难用它们来加速。

使用范围功能基本上需要手工矢量化,这对于仅处理ASCII的内容来说是很多工作。

正如您所发现的,在简单情况下,使用pcmpgtb和布尔逻辑可以更快。使用AVX2,您可以一次处理32个字节而不是16个字节,但是没有vpcmpistri的AVX2版本,只有16字节指令的AVX1 VEX编码。

,

正如Peter Cordes所指出的,该问题是由于对cmpstr输出的不必要依赖引起的。 这可以通过简单地重组此循环来解决:

const componentRef = useRef(null);
useEffect(() => {
    const {offsetHeight} = componentRef.current;
    console.log('componentRef height',offsetHeight);   
},[componentRef]);

return <>
    <YourComponent ref={componentRef} />
</>    

进入那个:

while (len - i >= 16) {
    __m128i v2 = _mm_loadu_si128((const __m128i*)(data + i));
    unsigned consumed = _mm_cmpistri(v1,v2,_SIDD_LEAST_SIGNIFICANT|_SIDD_CMP_RANGES|_SIDD_UBYTE_OPS|_SIDD_NEGATIVE_POLARITY);
    i += consumed;
    if (consumed < 16) {
        return i;
    }
}

使用if (len >= 16) while (i <= len - 16) { __m128i v2 = _mm_loadu_si128((const __m128i*)(data + i)); unsigned consumed = _mm_cmpistri(v1,_SIDD_LEAST_SIGNIFICANT|_SIDD_CMP_RANGES|_SIDD_UBYTE_OPS|_SIDD_NEGATIVE_POLARITY); if (consumed < 16) { return i + consumed; } i += 16; } 编译的i5-2410M的结果现在看起来好得多:

gcc -pedantic -Wall -Wextra -O3 -march=native sse42cmpstr.c

现在cmpistr显然比cmpestr和表搜索都快,甚至超过了 我测试过的大多数CPU上的手工SSE2逻辑比较。

完整的测试代码在这里:

Setting up 1GB of data...
table                - 484.5900ms
cmpestr              - 231.9770ms
cmpistr              - 121.3510ms
logic                - 142.3700ms
本文链接:https://www.f2er.com/3086295.html

大家都在问