1

Какая конструкция отработает быстрее:

if (a == 0 || b == 0)

или

if (a*b == 0)
Danis
  • 19,777
  • 6
  • 22
  • 56
CrazyMan
  • 35
  • 3

2 Answers2

7

скорее всего первая - будет сделано 2 cmp последовательно, а во втором случае все таки надо делать mul, хотя все равно это все однотиковые операции

но компиляторы сейчас умные, они и первый и второй вариант превратят во что-то одинаковое, как мне кажется

у меня студия сделала следующее (на максимальной оптимизации):

первый вариант:

00007FF7210D5B6C  cmp         dword ptr [rbp+0C0h],0  
00007FF7210D5B73  je          main+6Eh (07FF7210D5B7Eh)  
00007FF7210D5B75  cmp         dword ptr [rbp+0C4h],0  
00007FF7210D5B7C  jne         main+81h (07FF7210D5B91h) 

второй вариант

00007FF6DC065B6C  mov         eax,dword ptr [rbp+0C0h]  
00007FF6DC065B72  imul        eax,dword ptr [rbp+0C4h]  
00007FF6DC065B79  test        eax,eax  
00007FF6DC065B7B  jne         main+80h (07FF6DC065B90h) 

видать такой код вообще без разницы как делать :)

протестировал на таком коде (чтобы компилятор чего-то не соптимизировал лишнего):

int x, y;
std::cin >> x;
std::cin >> y;

if (x * y == 0) std::cout << "r";

P.S.

как по мне - это скорее всего экономия на спичках и лучше оставить первый вариант как более понятный

Zhihar
  • 37,513
  • А если поменять int на unsigned, второй вариант ухудшится? Хотя стоп, там же и так imul... – Qwertiy Feb 16 '21 at 02:09
4

А давайте протестируем. Тестовый код:

#include <iostream>
#include <ctime>
using std::cout;
using std::endl;

enum class func_method {log_or, mult};

template <typename test_type, func_method method> void test_func() { const unsigned long end = 4000000000;

volatile test_type x = 1;
volatile test_type y = 1;
unsigned int sum = 0;

auto clock_begin = std::clock();
if (method == func_method::log_or)
    for (unsigned long i = 0; i &lt; end; ++i)
    {
        test_type a = x;
        test_type b = y;
        if (a == 0 || b == 0)
            ++sum;
    }
else if (method == func_method::mult)
    for (unsigned long i = 0; i &lt; end; ++i)
    {
        test_type a = x;
        test_type b = y;
        if (a * b == 0)
            ++sum;
    }
else
    cout &lt;&lt; &quot;Invalid method&quot; &lt;&lt; endl;
auto clock_end = std::clock();

cout &lt;&lt; &quot;sum:  &quot; &lt;&lt; sum &lt;&lt; endl;
cout &lt;&lt; &quot;time: &quot; &lt;&lt; double(clock_end - clock_begin) / CLOCKS_PER_SEC &lt;&lt; endl;

}

int main() { typedef int test_type;

test_func&lt;test_type, func_method::mult&gt;();
test_func&lt;test_type, func_method::log_or&gt;();

return 0;

}

http://coliru.stacked-crooked.com (ссылки на примеры: int, unsigned int, long long, double), g++, количество итераций — 4'000'000'000, a == 1; b == 1, время в секундах:

test_type:    int         unsigned int         long long         double
умножение:    3.38705     3.47416              5.2172            5.07862
или:          4.98977     4.89922              4.95778           8.35183

Проверка с помощью умножения работает быстрее, чем проверка с помощью логического или, кроме типа long long.


https://ideone.com (ссылки на примеры: int, unsigned int, long long, double), clang 8.0, количество итераций — 2'000'000'000, a == 0; b == 1, время в секундах:

test_type:    int         unsigned int         long long         double
умножение:    0.996721    0.931937             0.918067          1.55657
или:          1.73786     1.69223              1.93048           2.8409

На ideone.com с использованием clang проверка с помощью умножения также стабильно опережает проверку с помощью логического или (даже при использовании типа long long, даже при переменной a равной нулю, т.е. не нужно проверять вторую часть условия b == 0)


Но это были примеры с фиксированными значениями переменных a и b. @avp заметил в комментариях, что время работы умножителя может зависеть от чисел, и предложил протестировать на случайных значениях.

Напишем небольшую вспомогательную функцию для генерации псевдослучайных чисел на основе линейного конгруэнтного метода:

std::uint32_t rand_value;

std::uint32_t lcg() { const auto a = 22695477u; const auto c = 1u;

rand_value = a * rand_value + c;
return rand_value;

}

Если test_type есть unsigned int, long long или double, то переменные a и b будем инициализировать так:

test_type a = lcg();
test_type b = lcg();

Если test_type есть 32-битный int, то a и b будем инициализировать так (чтобы избежать переполнения при вычислении произведения):

test_type a = lcg() & 32767;
test_type b = lcg() & 32767;

https://ideone.com (ссылки на примеры: int, unsigned int, long long, double), clang 8.0, количество итераций — 800'000'000, a == lcg() & mask; b == lcg() & mask, время в секундах:

test_type:    int         unsigned int         long long         double
умножение:    2.22886     2.02724              2.55701           2.05726
или:          2.36976     1.86795              2.30486           1.86942

Теперь проверка с помощью умножения работает медленнее. Хотя для типа int всё ещё быстрее (Разница во времени не велика, но это не похоже на погрешность при замере времени. Эксперимент проводил несколько раз, проверка через умножение для типа int в clang стабильно работает немного быстрее.)


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


P.S.

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

unsigned int a = 65536, b = 65536;
cout << a * b << endl; //0, если тип unsigned int - 32-битный
cout << (a == 0 || b == 0) << (a * b == 0) << endl; //01

double x = 0.0, y = std::numeric_limits<double>::infinity(); cout << x * y << endl; //NaN cout << (x == 0 || y == 0) << (x * y == 0) << endl; //10

wololo
  • 6,221
  • А если оптимизацию отключить? – TigerTV.ru Feb 15 '21 at 21:47
  • 1
    @TigerTV.ru, разница сократилась, но перемножение всё-таки быстрее. gcc с опцией -O0: int, double. Количество итераций правда пришлось уменьшить, т.к. не проходит по времени. – wololo Feb 15 '21 at 21:54
  • А если x, y брать random? Время работы умножителя (количество тактов) вообще-то может зависеть от чисел. – avp Feb 15 '21 at 22:34
  • @avp, вы правы, обновил ответ. – wololo Feb 16 '21 at 01:55
  • 1
    Есть у меня подозрение (обосновать не могу, и вообще - не уверен в нем, так что хотелось бы выслушать специалистов), не играет ли тут основную роль в смысле времени сами переходы?.. – Harry Feb 16 '21 at 06:44
  • @Harry, да, согласен, неплохо бы выслушать мнение специалистов :) – wololo Feb 16 '21 at 07:06
  • А еще явно должны играть роль вероятности равенства нулю a и b - например, если a крайне редко равно 0, а b - часто, то обратный порядок будет явно более быстрым. Словом, тут все очень сильно зависит от конкретных данных... – Harry Feb 16 '21 at 07:16
  • @Harry, вообще я думал, что для типа int компиляторы будут генерировать одинаковый код, но нет. Да ещё и способ с умножением иногда оказывается быстрее. Как-то это контринтуитивно. Хорошо бы увидеть не просто предположения/сферические замеры производительности в вакууме, а подробное объяснение наблюдаемого поведения. Но тут я не специалист. Кстати, вопрос про отличия CMP от TEST задавался здесь. В ответах пишут TEST вроде как быстрее. – wololo Feb 16 '21 at 09:54
  • 1
    Попробовал с random код для int и unsigned int на своем g++ -O3, Linux Mint, AMD Ryzen 5 3400G. Запустил каждую из 4-х раз по 10. С учетом разброса результатов разницы не заметил. (Уже интересно, да, тут думать надо) – avp Feb 16 '21 at 17:45