GCC генерирует тест на ненулевое значение, даже если нулевое значение гарантировано - PullRequest
0 голосов
/ 15 октября 2018

Рассмотрим следующий класс, созданный в основном для целей бенчмаркинга:

class String {
  char* data_; 
public:
  String(const char* arg = "") : data_(new char[strlen(arg) + 1]) { strcpy(data_, arg); }
  String(const String& other) : String(other.data_) { }
  String(String&& other) noexcept : data_(other.data_) { other.data_ = nullptr; }
  String& operator=(String other) noexcept { swap(other); return *this; }  
  ~String() { delete[] data_; }
  void swap(String& rhs) noexcept { std::swap(data_, rhs.data_); }
  const char* data() const { return data_; }   
};
void swap(String& lhs, String& rhs) noexcept { lhs.swap(rhs); }

Я пытаюсь сравнить эффективность обмена двух его экземпляров с пользовательскими swap и std::swap.Для пользовательских swap GCC 8.2 (-O2) создает следующую сборку x86_64:

mov     rax, QWORD PTR [rdi]
mov     rdx, QWORD PTR [rsi]
mov     QWORD PTR [rdi], rdx
mov     QWORD PTR [rsi], rax
ret

, которая точно соответствует обмену двумя указателями.Однако для std::swap сгенерированная сборка выглядит так:

  mov     rdx, QWORD PTR [rsi]
  mov     QWORD PTR [rsi], 0    // (A)
  mov     rax, QWORD PTR [rdi]
  mov     QWORD PTR [rdi], 0    // (1)
  mov     QWORD PTR [rsi], rax  // (B)
  mov     rax, QWORD PTR [rdi]  // (2)
  mov     QWORD PTR [rdi], rdx
  test    rax, rax              // (3)
  je      .L3
  mov     rdi, rax
  jmp     operator delete[](void*) 
.L3:
  ret

Мне интересно, почему GCC генерирует такой неэффективный код.Инструкция (1) устанавливает [rdi] в ноль.Этот ноль затем загружается в rax (2).И затем, rax проверяется (3), должен ли operator delete вызываться или нет.

Почему GCC проверяет rax, если он гарантированно равен нулю?Похоже, оптимизатору достаточно просто избежать этого теста.

Демонстрация Godbolt: https://godbolt.org/z/WNm2if


Другой источник неэффективности заключается в том, что сначала 0 записывается в [rsi] (A), а затем перезаписывается другим значением (B).

Итог: я ожидаю, что компилятор сгенерирует тот же машинный код для std::swap, а также для пользовательского swap, чего не происходит.Это указывает на то, что написание пользовательских функций обмена имеет смысл даже для классов, которые поддерживают семантику перемещения.

...