Какой самый быстрый способ обновить переменную в условии?
у меня есть указатель, 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 ответов:
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
будет скомпилирован в машинный код, который так быстро, как вы можете разумно ожидать, чтобы получить на. Если это не достаточно быстро, вам нужно начать думать о изменение то, что он делает. Я вижу два варианта там, один легкий, один не так просто:
изменить соглашение о вызовах:
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
- это поддерживать подстановку, но это не всегда возможно для того, чтобы сделать так.)изменить окружающий алгоритм, так что
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