Какой самый быстрый способ обновить переменную в условии?


у меня есть указатель, ptr, и условие cond. Мне нужен самый быстрый способ сбросить ptr Если cond - это true, или держать ptr без изменений, если cond - это false. Текущая реализация, тривиально:

void reset_if_true(void*& ptr, bool cond)
{
    if (cond)
        ptr = nullptr;
}

Я знаю, что вышеуказанная производительность кода хороша, и я не могу ожидать значительного повышения производительности, оптимизируя его. Однако этот код вызывается несколько миллионов раз в секунду, и каждая маленькая наносекунда сохраняется соответствующий.

Я думал о чем-то, что избавится от ветки, например:

void* p[] = { ptr, nullptr };
ptr = p[cond];

но я не уверен, что это лучший способ, чтобы продолжить.

5 55

5 ответов:

void reset_if_true(void*& ptr, bool cond)
{
    if (cond)
        ptr = nullptr;
}

наивное решение, несомненно, будет самым быстрым в большинстве случаев. Хотя он имеет ветвь, которая может быть медленной на современных конвейерных процессорах, это только медленно, если ветвь неверно истолковано. Поскольку предсказатели ветвей очень хороши в настоящее время, если только значение cond крайне непредсказуемо, вполне вероятно, что простая условная ветвь является самым быстрым способом написания кода.

а если нет, то хороший компилятор должны знайте это и сможете оптимизировать код до чего-то лучшего, учитывая целевую архитектуру. Который идет в точка gnasher729: просто напишите код простым способом и оставьте оптимизацию в руках оптимизатора.

а это хороший совет в целом, иногда это слишком далеко. Если вы действительно заботитесь о скорости этого кода, вам нужно проверить и посмотреть, что компилятор на самом деле делает с ним. Проверьте код объекта что он генерирует, и убедитесь, что это разумно и что код функции становится встроенным.

такое исследование может быть весьма показательным. Например, рассмотрим x86-64, где ветви могут быть довольно дорогими в тех случаях, когда предсказание ветвей сорвано (что действительно единственный раз, когда это интересный вопрос, поэтому давайте предположим, что cond совершенно непредсказуемо). Почти все компиляторы собираются генерировать следующее для наивных реализация:

reset_if_true(void*&, bool):
    test   sil, sil              ; test 'cond'
    je     CondIsFalse
    mov    QWORD PTR [rdi], 0    ; set 'ptr' to nullptr, and fall through
  CondIsFalse:
    ret

это примерно такой же жесткий код, как вы могли себе представить. Но если вы поместите предиктор ветви в патологический случай, он может оказаться медленнее, чем использование условного перемещения:

reset_if_true(void*&, bool):
    xor    eax, eax              ; pre-zero the register RAX
    test   sil, sil              ; test 'cond'
    cmove  rax, QWORD PTR [rdi]  ; if 'cond' is false, set the register RAX to 'ptr'
    mov    QWORD PTR [rdi], rax  ; set 'ptr' to the value in the register RAX
    ret                          ;  (which is either 'ptr' or 0)

условные перемещения имеют относительно высокую задержку, поэтому они значительно медленнее, чем хорошо предсказанная ветвь, но они могут быть быстрее, чем совершенно непредсказуемая ветвь. Вы ожидаете, что компилятор будет знать это при таргетинге на x86 архитектура, но она не имеет (по крайней мере, в этом простом примере) никаких знаний о cond'ы предсказуемость. Он предполагает простой случай, что предсказание ветвей будет на вашей стороне, и генерирует код A вместо кода B.

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

void reset_if_true_alt(void*& ptr, bool cond)
{
    ptr = (cond) ? nullptr : ptr;
}

это преуспевает в том, чтобы убедить современных версий лязгом для создания внеофисный код B, но является полной пессимизации в GCC и MSVC. Если бы вы не проверили сгенерированную сборку, вы бы этого не знали. Если вы хотите силу на GCC и MSVC генерирует код внеофисному, вам придется работать усерднее. Например, вы можете использовать вариант, опубликованный в вопросе:

void reset_if_true(void*& ptr, bool cond)
{
    void* p[] = { ptr, nullptr };
    ptr = p[cond];
}

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

reset_if_true_alt(void*&, bool):
    mov     rax, QWORD PTR [rdi]
    movzx   esi, sil
    mov     QWORD PTR [rsp-16], 0
    mov     QWORD PTR [rsp-24], rax
    mov     rax, QWORD PTR [rsp-24+rsi*8]
    mov     QWORD PTR [rdi], rax
    ret

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

если бы вы все еще отчаянно пытались устранить ветку на MSVC или GCC, вам пришлось бы сделать что-то более уродливое с участием переинтерпретация битов указателя и их скручивание. Что-то вроде:

void reset_if_true_alt(void*& ptr, bool cond)
{
    std::uintptr_t p = reinterpret_cast<std::uintptr_t&>(ptr);
    p &= -(!cond);
    ptr = reinterpret_cast<void*>(p);
}

это даст вам следующее:

reset_if_true_alt(void*&, bool):
    xor   eax, eax
    test  sil, sil
    sete  al
    neg   eax
    cdqe
    and   QWORD PTR [rdi], rax
    ret

опять же, здесь у нас есть больше инструкций, чем простая ветвь, но, по крайней мере, это инструкции с относительно низкой задержкой. Ориентир на реалистичные данные скажет вам, стоит ли компромисс. И дайте вам обоснование, которое вам нужно поместить в комментарий, если вы собираетесь на самом деле проверить код, как это.

как только я пошел вниз по кроличьей дыре, я смог заставить MSVC и GCC использовать условные инструкции по перемещению. По-видимому, они не делали эту оптимизацию, потому что мы имели дело с указателем:

void reset_if_true_alt(void*& ptr, bool cond)
{
    std::uintptr_t p = reinterpret_cast<std::uintptr_t&>(ptr);
    ptr = reinterpret_cast<void*>(cond ? 0 : p);
}
reset_if_true_alt(void*&, bool):
    mov    rax, QWORD PTR [rdi]
    xor    edx, edx
    test   sil, sil
    cmovne rax, rdx
    mov    QWORD PTR [rdi], rax
    ret

учитывая задержку CMOVNE и аналогичное количество инструкций, я не уверен, что это будет на самом деле быстрее, чем предыдущая версия. Тест, который вы запустили, сказал бы вам, если бы это было так.

аналогично, если мы немного скрутим условие, мы сохраним мы сами один доступ к памяти:

void reset_if_true_alt(void*& ptr, bool cond)
{
   std::uintptr_t c = (cond ? 0 : -1);
   reinterpret_cast<std::uintptr_t&>(ptr) &= c;
}
reset_if_true_alt(void*&, bool):
     xor    esi, 1
     movzx  esi, sil
     neg    rsi
     and    QWORD PTR [rdi], rsi
     ret

(это GCC. MSVC делает что-то немного другое, предпочитая свою характерную последовательность neg,sbb,neg и dec инструкции, но эти два морально эквивалентны. Clang преобразует его в тот же условный ход, который мы видели выше.) Это может быть лучший код, если нам нужно избегать ветвей, учитывая, что он генерирует разумный вывод на всех тестируемых компиляторах при сохранении (некоторые степень) читабельности в исходном коде.

самый низко висящий фрукт здесь не то, что вы думаете. Как обсуждалось в нескольких других ответах,reset_if_true будет скомпилирован в машинный код, который так быстро, как вы можете разумно ожидать, чтобы получить на. Если это не достаточно быстро, вам нужно начать думать о изменение то, что он делает. Я вижу два варианта там, один легкий, один не так просто:

  1. изменить соглашение о вызовах:

    template <class T>
    inline T* reset_if_true(T* ptr, bool condition)
    {
        return condition ? nullptr : ptr;
    }
    

    и затем измените вызывающего абонента(ов), чтобы прочитать что-то вроде

    ptr_var = reset_if_true(ptr_var, expression);
    

    это делает более вероятным, что ptr_var будет жить в регистре во время критического внутреннего цикла, который вызывает reset_if_true миллионы раз в секунду, и не будет никаких обращений к памяти, связанных с ним. ptr_var вытеснение в память-это самая дорогая вещь в вашем коде, как сейчас; даже дороже, чем потенциально неверно предсказанные ветви. (Достаточно хороший компилятор мая сделать это преобразование для вас предоставлены reset_if_true - это поддерживать подстановку, но это не всегда возможно для того, чтобы сделать так.)

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

    так как вы не сказали нам, что окружающий алгоритм, я не могу помочь вам с этим. Я могу, однако, сказать вам, что делать что-то, включающее проверку состояния миллионы раз в секунду, наверное указывает алгоритм с квадратичной временной сложностью или хуже, и это всегда означает, что вы должны хотя бы подумать о поиске лучшего. (Там не может быть лучше, увы.)

пока у нас есть sizeof(size_t) == sizeof(void*), nullptr представляется в двоичном виде как 0 и size_t, используя все биты (или имея std:: uintptr_t), вы можете сделать это:

// typedef std::uintptr_t ptrint_t; // uncomment if you have it
typedef size_t ptrint_t; // comment out if you have std::uintptr_t

void reset_if_true(void*& ptr, bool cond)
{
    ((ptrint_t&)ptr) &= -ptrint_t( !cond );
}

обратите внимание, однако, что время приведения от bool до size_t takes очень сильно зависит от реализации и может принимать ветвь сама по себе.

код абсолютно понятен.

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

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

обновление: я переопределил свой ответ.

в следующем коде идея заключается в преобразовании указателя в число и умножении его на число (cond). Примечание inline используется. Умножение может помочь использовать архитектуру, которая использует конвейерную обработку.

#include <cstdint>

template <typename T>
inline T* reset_if_true(T* p, bool cond) {
  void* ptr = (void*)p; // The optimising compiler (-O3) will get rid of unnecessary variables.
  intptr_t ptrint;
  // This is an unrecommended practice.
  ptrint = (intptr_t)ptr;
  ptrint = ptrint * cond;  // Multiply the integer
  void* ptr2 = (void*)ptrint;
  T* ptrv = (T*)ptr2;
  return ptrv;
}

пример использования:

#include <iostream>
#include <vector>

void test1(){
    //doulbe d = 3.141592;
    //typedef std::vector<double> mytype;
    std::vector<double> data = {3,1,4};
    auto ptr = &data;
    std::cout << (void*)ptr << std::endl;
    auto ptr2 = reset_if_true(ptr, 1);
    //auto ptr2 = (mytype*)reset_if_true(ptr, 1);
    std::cout << reset_if_true(ptr, 1) << " -> " << (*(reset_if_true(ptr, 1))).size() << std::endl;
    std::cout << reset_if_true(ptr, 2) << " -> "<< (*(reset_if_true(ptr, 2))).size() << std::endl;
    std::cout << reset_if_true(ptr, 0) <<
        " is null? " << (reset_if_true(ptr, 0) == NULL) <<  // Dont dereference a null.
        std::endl;
}


void test2(){
    double data = 3.141500123;
    auto ptr = &data;
    std::cout << (void*)ptr << std::endl;
    auto ptr2 = reset_if_true(ptr, 1);
    //auto ptr2 = (mytype*)reset_if_true(ptr, 1);
    std::cout << reset_if_true(ptr, 1) << " -> " << (*(reset_if_true(ptr, 1))) << std::endl;
    std::cout << reset_if_true(ptr, 2) << " -> "<< (*(reset_if_true(ptr, 2))) << std::endl;
    std::cout << reset_if_true(ptr, 0) <<
        " is null? " << (reset_if_true(ptr, 0) == NULL) <<  // Dont dereference a null.
        std::endl;

}

int main(){ test1(); test2(); }

компиляция с использованием этих флагов:-O3 -std=c++14. На выходе получается:

0x5690
0x5690 -> 3
0x5690 -> 3
0 is null? 1
0x5690
0x5690 -> 3.1415
0x5690 -> 3.1415
0 is null? 1

это может иметь проблемы с выравниванием памяти, когда такие параметры используются в командная строка компилятора -s FORCE_ALIGNED_MEMORY=1 . Также смотрите reinterpret_cast. Не забудьте использовать -O3.

cond может быть любым ненулевым значением. Здесь есть место для повышения производительности, если мы знаем, что это не что иное, как 0 или 1. В этом случае, вы можете использовать int другой целочисленный тип для cond.

PS. Это обновленный ответ. Предыдущий ответ, как я уже ясно упоминал в своем ответе, имел проблемы. Решение заключается в использовании intptr_t и конечно inline.

параметры компилятора использовать:

 em++ reset_if_true.cpp -O3 -std=c++14 -o reset_if_true.js
 node reset_if_true.js