Почему '==' медленнее на std::string?

При профилировании моего приложения я понял, что много времени тратится на сравнение строк. Поэтому я написал простой тест, и я был удивлен, что "==" намного медленнее, чем 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 = С++ 1y

Я использую gcc 4.9 на Linux-машине x86_64.

Очевидно, что использование -o3 делает некоторые оптимизации, которые, как я полагаю, полностью завершают последние две петли; однако, используя -o2, все же результаты выглядят странно:

за 1 миллиард итераций:

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

P.S. Таймер - это только класс обертки для измерения затраченного времени; Я абсолютно уверен в этом: 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_ */

Ответы

Ответ 1

UPDATE: выход улучшенного теста в 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

Обратите внимание, что в общем случае strcmp не является функционально эквивалентным == или .compare, когда текст может вставлять NUL, так как первый будет "рано выйти". (Это не причина, по которой он "быстрее" выше, но читайте ниже для комментариев, повторите возможные варианты с длиной строки/содержанием и т.д.)


Обсуждение/более ранний ответ

Просто взгляните на свою реализацию - например,

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

Найдите шаблон basic_string, затем для оператора ==, работающего с двумя экземплярами строки - mine:

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 для строк, часто отличающихся первым символом или тремя, но наоборот для длинных строк, которые равны или только отличаются ближе к концу, поэтому убедитесь, что вы попробуйте различные виды ввода, прежде чем вы поймете, что вы понимаете профиль производительности.

Ответ 2

Либо ваши тайминги являются отвратительными, либо ваш компилятор оптимизировал часть вашего кода из существования.

Подумайте об этом, десять миллиардов операций за 0.000349 миллисекунд (я буду использовать 0.000500 миллисекунд или половину микросекунды, чтобы упростить мои вычисления) означает, что вы выполняете двадцать триллионов операций в секунду.

Даже если одна операция может быть выполнена за один такт, это будет 20 000 ГГц, что немного превышает текущую культуру ЦП, даже с их оптимизированными по массе конвейерами и несколькими ядрами.

И, учитывая, что оптимизированные цифры -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), чтобы убедиться, что он фактически выполняет ту работу, которую он требует.

Ответ 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)); чтобы заставить компилятор выполнить сравнение. Я также добавил < <", a =" < чтобы заставить компилятор вычислить a.

Теперь вывод:

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

Можете ли вы объяснить, почему strcmp быстрее, чем .compare, который медленнее, чем ==? однако различия в скорости являются незначительными, но значительными.

Это на самом деле имеет смысл!: Р

Ответ 4

Ниже приведен неверный анализ скорости - благодаря Tony D для указания моей ошибки. Критика и советы для лучших тестов все еще применяются.


Все предыдущие ответы касаются проблем оптимизации компилятора в вашем тесте, но не отвечайте, почему strcmp все еще немного быстрее.

strcmp, скорее всего, быстрее (в скорректированных тестах) из-за строк, иногда содержащих нули. Поскольку strcmp использует C-строки, он может выйти, когда встречается завершение строки char '\0'. std::string::compare() рассматривает '\0' как еще один char и продолжается до конца массива строк.

Поскольку у вас есть недетерминированное сеяние RNG и генерируется только две строки, ваши результаты будут меняться при каждом запуске кода. (Я бы посоветовал это в тестах.) Учитывая цифры, 28 раз из 128, не должно быть никакого преимущества. В 10 раз из 128 вы получите более 10-кратное ускорение. И так далее.

Как и победить оптимизатор компилятора, я бы предположил, что в следующий раз вы создадите новую строку для каждой итерации сравнения, что позволит вам усреднить такие эффекты.

Ответ 5

Скомпилирован код gcc -O3 -S --std=c++1y. В результате здесь. Версия 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<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)