为啥'=='在std :: string上很慢?

Posted

技术标签:

【中文标题】为啥\'==\'在std :: string上很慢?【英文标题】:Why '==' is slow on std::string?为什么'=='在std :: string上很慢? 【发布时间】:2015-02-26 04:50:51 【问题描述】:

在分析我的应用程序时,我意识到很多时间都花在了字符串比较上。所以我写了一个简单的基准测试,我很惊讶 '==' 比 string::compare 和 strcmp 慢得多!这是代码,谁能解释这是为什么?或者我的代码有什么问题?因为根据标准 '==' 只是一个运算符重载,只是返回 !lhs.compare(rhs)。

#include <iostream>
#include <vector>
#include <string>
#include <stdint.h>
#include "Timer.h"
#include <random>
#include <time.h>
#include <string.h>
using namespace std;
uint64_t itr  = 10000000000;//10 Billion
int len = 100;
int main() 
  srand(time(0));
  string s1(len,random()%128);
  string s2(len,random()%128);

uint64_t a = 0;
  Timer t;
  t.begin();
  for(uint64_t i =0;i<itr;i++)
    if(s1 == s2)
      a = i;
  
  t.end();

  cout<<"==       took:"<<t.elapsedMillis()<<endl;

  t.begin();
  for(uint64_t i =0;i<itr;i++)
    if(s1.compare(s2)==0)
      a = i;
  
  t.end();

  cout<<".compare took:"<<t.elapsedMillis()<<endl;

  t.begin();
  for(uint64_t i =0;i<itr;i++)
    if(strcmp(s1.c_str(),s2.c_str()))
      a = i;
  
  t.end();

  cout<<"strcmp   took:"<<t.elapsedMillis()<<endl;

  return a;

结果如下:

==       took:5986.74
.compare took:0.000349
strcmp   took:0.000778

还有我的编译标志:

CXXFLAGS = -O3 -Wall -fmessage-length=0 -std=c++1y

我在 x86_64 linux 机器上使用 gcc 4.9。

显然使用 -o3 做了一些优化,我猜这会完全推出最后两个循环;但是,使用 -o2 的结果仍然很奇怪:

10 亿次迭代:

==       took:19591
.compare took:8318.01
strcmp   took:6480.35

附: Timer 只是一个用于测量花费时间的包装类;我对此非常肯定:D

Timer 类的代码:

#include <chrono>

#ifndef SRC_TIMER_H_
#define SRC_TIMER_H_


class Timer 
  std::chrono::steady_clock::time_point start;
  std::chrono::steady_clock::time_point stop;
public:
  Timer()
    start = std::chrono::steady_clock::now();
    stop = std::chrono::steady_clock::now();
  
  virtual ~Timer() 

  inline void begin() 
    start = std::chrono::steady_clock::now();
  

  inline void end() 
    stop = std::chrono::steady_clock::now();
  

  inline double elapsedMillis() 
    auto diff = stop - start;
    return  std::chrono::duration<double, std::milli> (diff).count();
  

  inline double elapsedMicro() 
    auto diff = stop - start;
    return  std::chrono::duration<double, std::micro> (diff).count();
  

  inline double elapsedNano() 
    auto diff = stop - start;
    return  std::chrono::duration<double, std::nano> (diff).count();
  

  inline double elapsedSec() 
    auto diff = stop - start;
    return std::chrono::duration<double> (diff).count();
  
;

#endif /* SRC_TIMER_H_ */

【问题讨论】:

如果您在一毫秒内看到任何事物的 100 亿次迭代,那么您真的应该质疑基准本身... 您的strcmp 代码错误。你应该在那里做== 0 如果您将== 基准换成.compare 基准,您会得到相同的结果吗? 我的电脑花了 6 多分钟才通过== 代码,6 分钟后我终止了程序。把100亿改成100万得到了这些结果:== took:37.5232.compare took:19.3218strcmp took:12.3108 我看了汇编语言。似乎对于 compare 和 strcmp 的情况,如果第一次迭代是!= 0,那么它完全退出循环。从语义上讲,这是正确的。 == 情况并非如此。我猜编译器真的很聪明,但还不够聪明。另外, == 不打电话。我猜它是内联的。这种内联可能导致优化出现问题。 【参考方案1】:

更新:http://ideone.com/rGc36a 的改进基准输出

==       took:21
.compare took:21
strcmp   took:14
==       took:21
.compare took:25
strcmp   took:14

证明让它有意义地工作的关键是“智取”编译器在编译时预测正在比较的字符串的能力:

// more strings that might be used...
string s[] =  len,argc+'A', len,argc+'A', len, argc+'B', len, argc+'B' ;

if(s[i&3].compare(s[(i+1)&3])==0)  // trickier to optimise
  a += i;  // cumulative observable side effects

请注意,当文本可能嵌入 NUL 时,strcmp 在功能上通常不等同于 ==.compare,因为前者会“提前退出”。 (这不是它在上面“更快”的原因,但请阅读下面的 cmets 可能会因字符串长度/内容等而发生变化。)


讨论/较早的答案

看看你的实现——例如

echo '#include <string>' > stringE.cc
g++ -E stringE.cc | less

搜索 basic_string 模板,然后搜索 operator== 处理两个字符串实例 - 我的是:

template<class _Elem,
    class _Traits,
    class _Alloc> inline
    bool __cdecl operator==(
            const basic_string<_Elem, _Traits, _Alloc>& _Left,
            const basic_string<_Elem, _Traits, _Alloc>& _Right)
    
    return (_Left.compare(_Right) == 0);
    

请注意,operator== 是内联的,只需调用 compare不可能在启用正常优化级别的情况下始终会明显变慢,尽管优化器可能偶尔会由于微妙的原因而更好地优化一个循环周围代码的副作用。

您的表面问题可能是由例如您的代码被优化超出了预期的工作点,for 循环任意展开到不同程度,或者优化或您的时间安排中的其他怪癖或错误。当您有不变的输入和没有任何累积副作用的循环时,这并不罕见(即编译器可以计算出不使用 a 的中间值,因此只有最后一个 a = i 需要生效)。

所以,学习编写更好的基准测试。在这种情况下,这有点棘手,因为内存中有许多不同的字符串准备调用比较,并以优化器在编译时无法预测的方式选择它们,但速度仍然足够快,不会压倒和掩盖影响的字符串比较代码,不是一件容易的事。此外,超出一点 - 比较分布在更多内存中的事物会使缓存影响与基准测试更相关,这进一步模糊了真正的比较性能。

不过,如果我是你,我会从文件中读取一些字符串 - 将每个字符串推送到 vector,然后遍历 vector,在相邻元素之间执行三个比较操作中的每一个。然后编译器不可能预测结果中的任何模式。您可能会发现 compare/==strcmp 更快/更慢,因为字符串的第一个或三个字符通常不同,但相反,对于在结尾处相等或仅不同的长字符串,请确保您尝试不同类型的输入,然后再得出结论您了解性能概况。

【讨论】:

请注意,C++ 中的 inline 与 C 一样,是对编译器的建议 您引用了错误的运算符==。右边是 operator==(const basic_string<_chart>& __lhs, const basic_string<_chart>& __rhs) @thang:不,我不是-我用作实现示例的标准库实现没有您提到的版本,并且 FWIW 21.4.8.2 中的 C++ 标准本身记录了类似于我引用的内容,没有单参数版本。您的库可能碰巧有这样的过载 - 如果不是 inline,那么请分享您的库,因为从性能角度来看这可能是相关的。 更新代码的问题在于,您现在正在测量运算符 [] 以及所有 & 和 % 的运行时间。另请注意,编译器仍然可以(并且很可能会)优化 a+i;另外,我使用 g++ v 4.8.1。我很确定这是正确的。我引用的 operator== 在传递之前先比较大小。这与汇编语言输出相匹配。您可能正在使用不同版本的库,但不太可能。 我会在完全不同的执行运行中进行这三个测试。【参考方案2】:

要么你的时间安排错了,要么你的编译器优化了一些不存在的代码。

想想看,100 亿次操作在 0.000349 毫秒内(我将使用 0.000500 毫秒或半微秒,以使我的计算更容易)意味着您每秒执行 20万亿次次操作.

即使一个操作可以在单个时钟周期内完成,也将是 20,000 GHz,略高于当前的 CPU,即使 拥有经过大规模优化的管道和多个内核。 p>

而且,鉴于-O2 优化后的数字彼此之间更加一致(== 花费的时间大约是compare 的两倍),“代码优化不存在”的可能性看起来要大得多。

时间翻倍可以很容易地解释为一百亿额外的函数调用,因为 operator== 需要调用 compare 来完成它的工作。

作为进一步的支持,请检查下表,以毫秒为单位显示数字(第三列是第二列的简单除以十的比例,因此第一列和第三列都用于十亿次迭代):

         -O2/1billion  -O3/10billion  -O3/1billion  Improvement
               (a)            (b)     (c = b / 10)    (a / c)
         ============  =============  ============  ===========
oper==          19151           5987           599           32
compare          8319         0.0005       0.00005  166,380,000

令人难以置信的是,-O3 可以将 == 代码加速大约 32 倍,但设法将 compare 代码加速几亿倍。


强烈建议您查看编译器生成的汇编代码(例如使用gcc -S 选项),以验证它实际上正在工作它声称这样做。

【讨论】:

确实如此,但请看一下 -o2 结果。 我也放了计时器代码。我想你可以自己运行它或使用 gettimeofday 或其他东西。 @Behrooz:或者您可以按照我的建议进行操作并检查编译器输出的代码。我已经解释过,你看到的结果几乎是不可能的。由于检查代码需要您而不是我的工作,这将是我的首选:-) 所以我检查了代码的可禁用性,它实际上产生了正确的代码!我使用了标志,这是相关部分的组件:pastebin.com/tyjSQYFT BTW,这是这个二进制文件的结果:比较 take:8825.2 == take:20729.9 strcmp take:6806.52【参考方案3】:

问题在于编译器正在对您的代码进行大量优化。

这是修改后的代码:

#include <iostream>
#include <vector>
#include <string>
#include <stdint.h>
#include "Timer.h"
#include <random>
#include <time.h>
#include <string.h>
using namespace std;
uint64_t itr  = 500000000;//10 Billion
int len = 100;
int main() 
  srand(time(0));
  string s1(len,random()%128);
  string s2(len,random()%128);

uint64_t a = 0;
  Timer t;
  t.begin();
  for(uint64_t i =0;i<itr;i++)
asm volatile("" : "+g"(s2));
    if(s1 == s2)
      a += i;
  
  t.end();

  cout<<"==       took:"<<t.elapsedMillis()<<",a="<<a<<endl;

  t.begin();
  for(uint64_t i =0;i<itr;i++)
asm volatile("" : "+g"(s2));
    if(s1.compare(s2)==0)
      a+=i;
  
  t.end();

  cout<<".compare took:"<<t.elapsedMillis()<<",a="<<a<<endl;

  t.begin();
  for(uint64_t i =0;i<itr;i++)
asm volatile("" : "+g"(s2));
    if(strcmp(s1.c_str(),s2.c_str()) == 0)
      a+=i;
  
  t.end();

  cout<<"strcmp   took:"<<t.elapsedMillis()<<",a="<<a<< endl;

  return a;

我添加了 asm volatile("" : "+g"(s2)); 以强制编译器运行比较。我还添加了

现在的输出是:

==       took:10221.5,a=0
.compare took:10739,a=0
strcmp   took:9700,a=0

你能解释一下为什么 strcmp 比 .compare 快,而 .compare 比 == 慢吗?但是,速度差异很小,但很重要。

确实有道理! :p

【讨论】:

@TonyD,编译器无法控制缓存。那是处理器属性。不,这不会强制处理器刷新缓存。 抱歉 - 写得太快了(!) - 它阻止编译器使用缓存在寄存器中的任何字符串状态,这意味着它必须再次从 L1 获取数据。我谈到了错误的缓存级别,但这仍然是一个潜在的问题。 您不能将整个字符串放入寄存器中。你说的毫无意义。如果您想检查,请查看汇编语言输出。 字符串对象具有可以在寄存器中的开始/结束/容量指针成员。无论如何 - 我已经在这个问题上花费了太多时间 - 将把它留给任何感兴趣/关心的人。干杯。【参考方案4】:

下面的速度分析是错误的 - 感谢 Tony D 指出我的错误。不过,对更好基准的批评和建议仍然适用。


之前的所有答案都涉及您的基准测试中的编译器优化问题,但不要回答为什么strcmp 仍然稍微快一些。

strcmp 可能更快(在更正的基准测试中),因为字符串有时包含零。由于strcmp 使用C 字符串,它可以在遇到字符串终止字符'\0' 时退出。 std::string::compare()'\0' 视为另一个字符并一直持续到字符串数组的末尾。

由于您对 RNG 进行了不确定的播种,并且只生成了两个字符串,因此您的结果会随着代码的每次运行而改变。 (我建议不要在基准测试中这样做。)鉴于数字,128 次中有 28 次,应该没有优势。 128 次中有 10 次你将获得超过 10 倍的加速。以此类推。

除了击败编译器的优化器之外,我还建议您下次为每次比较迭代生成一个新字符串,以便平均消除这些影响。

【讨论】:

“128 次中有 28 次,应该没有优势。128 次中有 10 次你将获得超过 10 倍的加速” - 你能解释一下吗那个分析?每个字符串包含 0 到 127 之间的某个字符的 100 次重复...唯一的比较函数超过第一个字符的情况是两个字符串相同(128 中的 1),并且只有当它们都是 NUL (128^2 中有 1 个)strcmp 会提前退出,而 ==.compare 不会。【参考方案5】:

使用gcc -O3 -S --std=c++1y 编译代码。结果是here。 gcc 版本是:

gcc (Ubuntu 4.9.1-16ubuntu6) 4.9.1
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

你看,我们可以是第一个循环(operator ==)是这样的:(评论是我加的)

    movq    itr(%rip), %rbp
    movq    %rax, %r12
    movq    %rax, 56(%rsp)
    testq   %rbp, %rbp
    je  .L25
    movq    16(%rsp), %rdi
    movq    32(%rsp), %rsi
    xorl    %ebx, %ebx
    movq    -24(%rsi), %rdx  ; length of string1
    cmpq    -24(%rdi), %rdx  ; compare lengths
    je  .L53                 ; compare content only when length is the same
.L10
   ; end of loop, print out follows

;....
.L53:
    .cfi_restore_state
    call    memcmp      ; compare content
    xorl    %edx, %edx  ; zero loop count
    .p2align 4,,10
    .p2align 3
.L13:
    testl   %eax, %eax  ; check result
    cmove   %rdx, %rbx  ; a = i
    addq    $1, %rdx    ; i++
    cmpq    %rbp, %rdx  ; i < itr?
    jne .L13
    jmp .L10    

; ....
.L25:
    xorl    %ebx, %ebx
    jmp .L10

我们可以看到operator == 是内联的,只有对memcmp 的调用。而对于operator ==,如果长度不同,则内容不进行比较。

最重要的是,比较只进行一次。循环内容仅包含i++;a=i;i&lt;itr;

对于第二个循环 (compare()):

    movq    itr(%rip), %r12
    movq    %rax, %r13
    movq    %rax, 56(%rsp)
    testq   %r12, %r12
    je  .L14
    movq    16(%rsp), %rdi
    movq    32(%rsp), %rsi
    movq    -24(%rdi), %rbp
    movq    -24(%rsi), %r14  ; read and compare length
    movq    %rbp, %rdx
    cmpq    %rbp, %r14
    cmovbe  %r14, %rdx       ; save the shorter length of the two string to %rdx
    subq    %r14, %rbp       ; length difference in %rbp
    call    memcmp           ; content is always compared
    movl    $2147483648, %edx ; 0x80000000 sign extended
    addq    %rbp, %rdx       ; revert the sign bit of %rbp (length difference) and save to %rdx
    testl   %eax, %eax       ; memcmp returned 0?
    jne .L14                 ; no, string different
    testl   %ebp, %ebp       ; memcmp returned 0. Are lengths the same (%ebp == 0)?
    jne .L14                 ; no, string different
    movl    $4294967295, %eax ; string compare equal
    subq    $1, %r12         ; itr - 1
    cmpq    %rax, %rdx
    cmovbe  %r12, %rbx       ; a = itr - 1
.L14:
    ; output follows

这里根本没有循环。

compare() 中,由于它应该根据比较返回正、负或零,因此始终比较字符串内容。 memcmp 调用了一次。

对于第三个循环(strcmp()),组装是最简单的:

    movq    itr(%rip), %rbp   ; itr to %rbp
    movq    %rax, %r12
    movq    %rax, 56(%rsp)
    testq   %rbp, %rbp
    je  .L16
    movq    32(%rsp), %rsi
    movq    16(%rsp), %rdi
    subq    $1, %rbp       ; itr - 1 to %rbp
    call    strcmp
    testl   %eax, %eax     ; test compare result
    cmovne  %rbp, %rbx     ; if not equal, save itr - 1 to %rbx (a)
.L16:

这些也根本没有循环。 strcmp 被调用,如果字符串不相等(如您的代码中所示),则将itr-1 直接保存到a

因此您的基准测试无法测试operator ==compare()strcmp() 的运行时间。都只调用一次,无法显示运行时差。

至于operator ==为什么占用的时间最多,是因为对于operator==,编译器由于某种原因没有消除循环。循环需要时间(但循环根本不包含字符串比较)。

从显示的程序集中,我们可以假设operator == 可能是最快的,因为如果两个字符串的长度不同,它根本不会进行字符串比较。 (当然在gcc4.9.1 -O3下)

【讨论】:

以上是关于为啥'=='在std :: string上很慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 kafka 生产者在第一条消息上很慢?

MySQL简单插入查询在“查询结束”步骤上很慢

高级自定义字段在管理屏幕上很慢

git 在我的 vagrant centos 机器上很慢

为啥在 C# 中的二维数组中按列写入速度很慢

为啥 std::string append 在 rval ref 上没有重载?