Почему __restrict часто не дает ожидаемой оптимизации?

#include <iostream>
#include <chrono>

void foo(int* a, int* b) {
  *a+=1;
  *b+=1;
  *a+=1;
}

void goo(int* __restrict a, int* b) {
  *a+=1;
  *b+=1;
  *a+=1;
}

void measure() {
  int x = 1;
  int y = 2;

  auto start_foo = std::chrono::high_resolution_clock::now();  
 
  for(int i = 0; i < 10000000; ++i) {
    foo(&x, &y);
  }
  
  auto end_foo = std::chrono::high_resolution_clock::now();
  
  
  std::chrono::duration<double> duration_foo = end_foo - start_foo;
  std::cout << "foo Runtime(secs): " << duration_foo.count() << std::endl;

  auto start_goo = std::chrono::high_resolution_clock::now();

  for(int i = 0; i < 10000000; ++i) {
    goo(&x, &y);
  }

  auto end_goo = std::chrono::high_resolution_clock::now(); 
  std::chrono::duration<double> duration_goo = end_goo - start_goo;

  std::cout << "goo Runtime(secs): " << duration_goo.count() << std::endl;
  std::cout << "Without/With: " << duration_foo.count() / duration_goo.count() << std::endl << std::endl;
}

int main() {
  for (int i = 0; i < 10; ++i) {
    measure();
  }
}

Я вижу, что goo оптимизирован, как и ожидалось, через __restrict, но результат меня сильно смутил, поскольку бывают случаи, когда goo работает намного хуже, чем foo. В 3 из 10 случаев они работают одинаково, в 3 из 10 случаев foo работает лучше, и только в 4 из 10 случаев мы ожидаем результат.

Пожалуйста, объясните, почему?

foo(int*, int*):
        add     DWORD PTR [rdi], 1
        add     DWORD PTR [rsi], 1
        add     DWORD PTR [rdi], 1
        ret
goo(int*, int*):
        add     DWORD PTR [rsi], 1
        add     DWORD PTR [rdi], 2
        ret
.LC1:
        .string "foo Runtime(secs): "
.LC2:
        .string "goo Runtime(secs): "
.LC3:
        .string "Without/With: "
measure():
        push    rbp
        push    rbx
        sub     rsp, 24
        call    std::chrono::_V2::system_clock::now()
        mov     rbx, rax
        call    std::chrono::_V2::system_clock::now()
        mov     edx, 19
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:std::cout
        sub     rax, rbx
        pxor    xmm0, xmm0
        cvtsi2sdq       xmm0, rax
        divsd   xmm0, QWORD PTR .LC0[rip]
        movsd   QWORD PTR [rsp], xmm0
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        movsd   xmm0, QWORD PTR [rsp]
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
        mov     rbp, rax
        mov     rax, QWORD PTR [rax]
        mov     rax, QWORD PTR [rax-24]
        mov     rbx, QWORD PTR [rbp+240+rax]
        test    rbx, rbx
        je      .L9
        cmp     BYTE PTR [rbx+56], 0
        je      .L7
        movsx   esi, BYTE PTR [rbx+67]
.L8:
        mov     rdi, rbp
        call    std::basic_ostream<char, std::char_traits<char> >::put(char)
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::flush()
        call    std::chrono::_V2::system_clock::now()
        mov     rbx, rax
        call    std::chrono::_V2::system_clock::now()
        mov     edx, 19
        mov     esi, OFFSET FLAT:.LC2
        mov     edi, OFFSET FLAT:std::cout
        sub     rax, rbx
        pxor    xmm0, xmm0
        cvtsi2sdq       xmm0, rax
        divsd   xmm0, QWORD PTR .LC0[rip]
        movsd   QWORD PTR [rsp+8], xmm0
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        movsd   xmm0, QWORD PTR [rsp+8]
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
        mov     rbp, rax
        mov     rax, QWORD PTR [rax]
        mov     rax, QWORD PTR [rax-24]
        mov     rbx, QWORD PTR [rbp+240+rax]
        test    rbx, rbx
        je      .L9
        cmp     BYTE PTR [rbx+56], 0
        je      .L10
        movsx   esi, BYTE PTR [rbx+67]
.L11:
        mov     rdi, rbp
        call    std::basic_ostream<char, std::char_traits<char> >::put(char)
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::flush()
        mov     edx, 14
        mov     esi, OFFSET FLAT:.LC3
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edi, OFFSET FLAT:std::cout
        movsd   xmm0, QWORD PTR [rsp]
        divsd   xmm0, QWORD PTR [rsp+8]
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
        mov     rbp, rax
        mov     rax, QWORD PTR [rax]
        mov     rax, QWORD PTR [rax-24]
        mov     rbx, QWORD PTR [rbp+240+rax]
        test    rbx, rbx
        je      .L9
        cmp     BYTE PTR [rbx+56], 0
        je      .L12
        movsx   esi, BYTE PTR [rbx+67]
.L13:
        mov     rdi, rbp
        call    std::basic_ostream<char, std::char_traits<char> >::put(char)
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::flush()
        mov     rbp, rax
        mov     rax, QWORD PTR [rax]
        mov     rax, QWORD PTR [rax-24]
        mov     rbx, QWORD PTR [rbp+240+rax]
        test    rbx, rbx
        je      .L9
        cmp     BYTE PTR [rbx+56], 0
        je      .L14
        movsx   esi, BYTE PTR [rbx+67]
.L15:
        mov     rdi, rbp
        call    std::basic_ostream<char, std::char_traits<char> >::put(char)
        add     rsp, 24
        pop     rbx
        mov     rdi, rax
        pop     rbp
        jmp     std::basic_ostream<char, std::char_traits<char> >::flush()
.L7:
        mov     rdi, rbx
        call    std::ctype<char>::_M_widen_init() const
        mov     rax, QWORD PTR [rbx]
        mov     esi, 10
        mov     rax, QWORD PTR [rax+48]
        cmp     rax, OFFSET FLAT:_ZNKSt5ctypeIcE8do_widenEc
        je      .L8
        mov     rdi, rbx
        call    rax
        movsx   esi, al
        jmp     .L8
.L10:
        mov     rdi, rbx
        call    std::ctype<char>::_M_widen_init() const
        mov     rax, QWORD PTR [rbx]
        mov     esi, 10
        mov     rax, QWORD PTR [rax+48]
        cmp     rax, OFFSET FLAT:_ZNKSt5ctypeIcE8do_widenEc
        je      .L11
        mov     rdi, rbx
        call    rax
        movsx   esi, al
        jmp     .L11
.L12:
        mov     rdi, rbx
        call    std::ctype<char>::_M_widen_init() const
        mov     rax, QWORD PTR [rbx]
        mov     esi, 10
        mov     rax, QWORD PTR [rax+48]
        cmp     rax, OFFSET FLAT:_ZNKSt5ctypeIcE8do_widenEc
        je      .L13
        mov     rdi, rbx
        call    rax
        movsx   esi, al
        jmp     .L13
.L14:
        mov     rdi, rbx
        call    std::ctype<char>::_M_widen_init() const
        mov     rax, QWORD PTR [rbx]
        mov     esi, 10
        mov     rax, QWORD PTR [rax+48]
        cmp     rax, OFFSET FLAT:_ZNKSt5ctypeIcE8do_widenEc
        je      .L15
        mov     rdi, rbx
        call    rax
        movsx   esi, al
        jmp     .L15
.L9:
        call    std::__throw_bad_cast()
main:
        push    rbx
        mov     ebx, 10
.L35:
        call    measure()
        sub     ebx, 1
        jne     .L35
        xor     eax, eax
        pop     rbx
        ret
_GLOBAL__sub_I_foo(int*, int*):
        sub     rsp, 8
        mov     edi, OFFSET FLAT:std::__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:std::__ioinit
        mov     edi, OFFSET FLAT:std::ios_base::Init::~Init() [complete object destructor]
        add     rsp, 8
        jmp     __cxa_atexit
.LC0:
        .long   0
        .long   1104006501

goo Runtime(secs): 7e-08
Without/With: 2.14286

foo Runtime(secs): 4e-08
goo Runtime(secs): 4e-08
Without/With: 1

foo Runtime(secs): 4e-08
goo Runtime(secs): 3e-08
Without/With: 1.33333

foo Runtime(secs): 4e-08
goo Runtime(secs): 3e-08
Without/With: 1.33333

foo Runtime(secs): 4e-08
goo Runtime(secs): 4e-08
Without/With: 1

foo Runtime(secs): 4e-08
goo Runtime(secs): 9e-08
Without/With: 0.444444

foo Runtime(secs): 3e-08
goo Runtime(secs): 4e-08
Without/With: 0.75

foo Runtime(secs): 4e-08
goo Runtime(secs): 4e-08
Without/With: 1

foo Runtime(secs): 3e-08
goo Runtime(secs): 4e-08
Without/With: 0.75

foo Runtime(secs): 4e-08
goo Runtime(secs): 3e-08
Without/With: 1.33333

Помимо всего остального, что здесь было сказано, вам, вероятно, следует __restrict оба параметра.

catnip 17.06.2024 09:38

На самом деле, где бы я ни видел использование __restirct, он использовался только с одним параметром.

Hayk 17.06.2024 09:49

Оптимизатор видит, что ваш распорядок дня не имеет побочных эффектов, и уничтожает его. Самый простой способ обойти это — объявить foo и goo как возвращающие целое число и вычислить сумму возвращаемых значений. Даже в этом случае вам, возможно, придется добавить условный тест (который, как вы знаете, никогда не будет удовлетворен) и распечатку результата, чтобы самые агрессивные оптимизаторы не уничтожали тестируемый код внутри цикла. Для такой простой задачи, как суммирование последовательных целых чисел, я бы не стал исключать возможность замены некоторыми из лучших на сегодняшний день оптимизирующих компиляторов формулой N*(N+1)/2.

Martin Brown 17.06.2024 10:08

Добавление volatile сообщает оптимизатору, что чтение или запись абсолютно необходимы. Это распространенный способ выборочного отключения оптимизации. Но он напрямую взаимодействует с __restrict — один сообщает оптимизатору, что чтения оптимизировать нельзя, а другой говорит, что чтения можно оптимизировать. Здесь, если оба a и b сделаны изменчивыми, __restrict ничего не делает.

MSalters 17.06.2024 10:56

Вы можете поместить функции в другое TU, чтобы компилятор не видел их определений. Обязательно также отключите оптимизацию времени соединения.

Weijun Zhou 17.06.2024 13:19
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
111
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как показывает дизассемблирование, два вызова high_resolution_clock::now() разделены только одним mov, и это для сохранения результата первого now(). Ничего не измеряется.

Проблема в том, что в -O3 оптимизатор видит, что ваш код ничего не делает. А на более низких уровнях оптимизации вы не сможете измерить ничего значимого. __restrict явно предназначен для оптимизатора. Это объясняет, почему это так редко бывает полезно: часто оптимизатор может оптимизировать без него (лишне), или оптимизатор все еще не может оптимизировать с ним (недостаточно).

Другие вопросы по теме