Подтвердить что ты не робот

Арифметические операции над булевыми операциями

Я нашел этот фрагмент кода на каком-то форуме:

if ( a * b * c * d == 0 ) ....

и владелец утверждает, что это немного быстрее, чем

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

Эти переменные определяются как:

int a, b, c, d;

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

Если мы просто игнорируем readability и просто фокусируемся на производительности, верно ли утверждение?

Мне кажется, что второй подход может быть быстрее, потому что иногда вы можете использовать "короткое замыкание". Но тогда, что-я-знаю?!

4b9b3361

Ответ 1

В стандарте C ничего не говорится о производительности. Вопрос о том,

if ( a * b * c * d == 0 )

быстрее, чем

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

имеет смысл только в контексте конкретного кода генерации компилятора, работающего на конкретной машине. Единственный реальный способ сравнить их - измерить производительность в вашей собственной системе или в любой системе, в которой вы заинтересованы.

Тем не менее, мы можем размышлять о том, какова будет производительность.

Как вы сказали, a, b, c и d являются объектами типа int. Вы также сказали, что они находятся в диапазоне [-100, + 100] - но компилятор не обязательно это знает.

Компилятор может заменить любое выражение кодом, который делает то же самое.

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

Вы говорите, что значения достаточно малы, что переполнение не является проблемой. На самом деле вы не можете переносить это предположение; INT_MAX может быть меньше 32767. Но компилятор знает, насколько большой int в системе, для которой он генерирует код. Тем не менее, если у него нет информации о значениях a, b, c и d, он не может предположить, что переполнения не будет.

За исключением того, что да, на самом деле, это может сделать это предположение. Поведение знакового целочисленного переполнения undefined. Это дает оптимизирующему разрешению компилятора предположить, что переполнение не может произойти (если это так, любое поведение, которое показывает программа, все равно).

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

Что касается другого выражения, a == 0 || b == 0 || c == 0 || d == 0, оператор || имеет короткозамкнутую семантику; если левый операнд имеет значение true (отличное от нуля), то правый операнд не оценивается. И такой условный код может создавать проблемы с производительностью из-за проблем с конвейером CPU. Поскольку ни один из подвыражений не имеет побочных эффектов (при условии, что ни одна из переменных не объявлена ​​volatile), компилятор может оценить все четыре подвыражения, возможно, параллельно, если это быстрее.

Быстрый эксперимент показывает, что gcc -O3 для x86 не выполняет ни оптимизации. Для первого выражения он генерирует код, который выполняет три умножения. Во-вторых, он генерирует условные ветки, реализуя канонические оценки короткого замыкания (я не знаю, будет ли избегать этого быстрее или нет).

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

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

Первое правило оптимизации программы: не делайте этого. Второе правило оптимизации программ (только для экспертов!): Не делайте этого еще.

- Майкл А. Джексон

Ответ 2

Эти два не эквивалентны. Например, на моей машине (32-разрядный x86 MSVC), если a, b, c и d равны 0x100, тогда первый тест пройдет, но второе условие не будет.

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

EDIT: Код, сгенерированный для первой версии:

00401000 8B 44 24 04      mov         eax,dword ptr [esp+4] 
00401004 0F AF 44 24 08   imul        eax,dword ptr [esp+8] 
00401009 0F AF 44 24 0C   imul        eax,dword ptr [esp+0Ch] 
0040100E 0F AF 44 24 10   imul        eax,dword ptr [esp+10h] 
00401013 85 C0            test        eax,eax 
00401015 75 07            jne         f1+1Eh (40101Eh) 
00401017 ...

Код, сгенерированный для второй версии:

00401020 83 7C 24 04 00   cmp         dword ptr [esp+4],0 
00401025 74 15            je          f2+1Ch (40103Ch) 
00401027 83 7C 24 08 00   cmp         dword ptr [esp+8],0 
0040102C 74 0E            je          f2+1Ch (40103Ch) 
0040102E 83 7C 24 0C 00   cmp         dword ptr [esp+0Ch],0 
00401033 74 07            je          f2+1Ch (40103Ch) 
00401035 83 7C 24 10 00   cmp         dword ptr [esp+10h],0 
0040103A 75 07            jne         f2+23h (401043h) 
0040103C ...

Тесты на моей машине (в наносекундах): первая версия работает примерно в 1,83 нс, а вторая - около 1,39 нс. Значения a, b, c и d не изменялись во время каждого прогона, поэтому, по-видимому, предсказатель ветвления мог предсказать 100% ветвей.

Ответ 3

Как обычно, с чем возникают более быстрые вопросы, что вы пробовали? Вы собирали и разбирали и видели, что происходит?

unsigned int mfun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d )
{
    if ( a * b * c * d == 0 ) return(7);
    else return(11);
}

unsigned int ofun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d )
{
    if (a == 0 || b == 0 || c == 0 || d == 0) return(7);
    else return(11);
}

для компилятора arm one дает это

00000000 <mfun>:
   0:   e0010190    mul r1, r0, r1
   4:   e0020291    mul r2, r1, r2
   8:   e0110293    muls    r1, r3, r2
   c:   13a0000b    movne   r0, #11
  10:   03a00007    moveq   r0, #7
  14:   e12fff1e    bx  lr

00000018 <ofun>:
  18:   e3500000    cmp r0, #0
  1c:   13510000    cmpne   r1, #0
  20:   0a000004    beq 38 <ofun+0x20>
  24:   e3520000    cmp r2, #0
  28:   13530000    cmpne   r3, #0
  2c:   13a0000b    movne   r0, #11
  30:   03a00007    moveq   r0, #7
  34:   e12fff1e    bx  lr
  38:   e3a00007    mov r0, #7
  3c:   e12fff1e    bx  lr

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

mips дали мне это

00000000 <mfun>:
   0:   00a40018    mult    a1,a0
   4:   00002012    mflo    a0
    ...
  10:   00860018    mult    a0,a2
  14:   00002012    mflo    a0
    ...
  20:   00870018    mult    a0,a3
  24:   00002012    mflo    a0
  28:   10800003    beqz    a0,38 <mfun+0x38>
  2c:   00000000    nop
  30:   03e00008    jr  ra
  34:   2402000b    li  v0,11
  38:   03e00008    jr  ra
  3c:   24020007    li  v0,7

00000040 <ofun>:
  40:   10800009    beqz    a0,68 <ofun+0x28>
  44:   00000000    nop
  48:   10a00007    beqz    a1,68 <ofun+0x28>
  4c:   00000000    nop
  50:   10c00005    beqz    a2,68 <ofun+0x28>
  54:   00000000    nop
  58:   10e00003    beqz    a3,68 <ofun+0x28>
  5c:   00000000    nop
  60:   03e00008    jr  ra
  64:   2402000b    li  v0,11
  68:   03e00008    jr  ra
  6c:   24020007    li  v0,7

Если ветки слишком дороги, равные и орлы выглядят быстрее.

Openrisc 32

00000000 <mfun>:
   0:   e0 64 1b 06     l.mul r3,r4,r3
   4:   e0 a3 2b 06     l.mul r5,r3,r5
   8:   e0 c5 33 06     l.mul r6,r5,r6
   c:   bc 26 00 00     l.sfnei r6,0x0
  10:   0c 00 00 04     l.bnf 20 <mfun+0x20>
  14:   9d 60 00 0b     l.addi r11,r0,0xb
  18:   44 00 48 00     l.jr r9
  1c:   15 00 00 00     l.nop 0x0
  20:   44 00 48 00     l.jr r9
  24:   9d 60 00 07     l.addi r11,r0,0x7

00000028 <ofun>:
  28:   e0 e0 20 02     l.sub r7,r0,r4
  2c:   e0 87 20 04     l.or r4,r7,r4
  30:   bd 64 00 00     l.sfgesi r4,0x0
  34:   10 00 00 10     l.bf 74 <ofun+0x4c>
  38:   e0 80 18 02     l.sub r4,r0,r3
  3c:   e0 64 18 04     l.or r3,r4,r3
  40:   bd 63 00 00     l.sfgesi r3,0x0
  44:   10 00 00 0c     l.bf 74 <ofun+0x4c>
  48:   e0 60 30 02     l.sub r3,r0,r6
  4c:   e0 c3 30 04     l.or r6,r3,r6
  50:   bd 66 00 00     l.sfgesi r6,0x0
  54:   10 00 00 08     l.bf 74 <ofun+0x4c>
  58:   e0 60 28 02     l.sub r3,r0,r5
  5c:   e0 a3 28 04     l.or r5,r3,r5
  60:   bd 85 00 00     l.sfltsi r5,0x0
  64:   0c 00 00 04     l.bnf 74 <ofun+0x4c>
  68:   9d 60 00 0b     l.addi r11,r0,0xb
  6c:   44 00 48 00     l.jr r9
  70:   15 00 00 00     l.nop 0x0
  74:   44 00 48 00     l.jr r9
  78:   9d 60 00 07     l.addi r11,r0,0x7

это зависит от реализации умножения, если это один такт, то умножения имеют его.

Если ваше оборудование не поддерживает многократное умножение, вы должны сделать вызов, чтобы он имитировал

00000000 <mfun>:
   0:   0b 12           push    r11     
   2:   0a 12           push    r10     
   4:   09 12           push    r9      
   6:   09 4d           mov r13,    r9  
   8:   0b 4c           mov r12,    r11 
   a:   0a 4e           mov r14,    r10 
   c:   0c 4f           mov r15,    r12 
   e:   b0 12 00 00     call    #0x0000 
  12:   0a 4e           mov r14,    r10 
  14:   0c 49           mov r9, r12 
  16:   b0 12 00 00     call    #0x0000 
  1a:   0a 4e           mov r14,    r10 
  1c:   0c 4b           mov r11,    r12 
  1e:   b0 12 00 00     call    #0x0000 
  22:   0e 93           tst r14     
  24:   06 24           jz  $+14        ;abs 0x32
  26:   3f 40 0b 00     mov #11,    r15 ;#0x000b
  2a:   39 41           pop r9      
  2c:   3a 41           pop r10     
  2e:   3b 41           pop r11     
  30:   30 41           ret         
  32:   3f 40 07 00     mov #7, r15 ;#0x0007
  36:   39 41           pop r9      
  38:   3a 41           pop r10     
  3a:   3b 41           pop r11     
  3c:   30 41           ret         

0000003e <ofun>:
  3e:   0f 93           tst r15     
  40:   09 24           jz  $+20        ;abs 0x54
  42:   0e 93           tst r14     
  44:   07 24           jz  $+16        ;abs 0x54
  46:   0d 93           tst r13     
  48:   05 24           jz  $+12        ;abs 0x54
  4a:   0c 93           tst r12     
  4c:   03 24           jz  $+8         ;abs 0x54
  4e:   3f 40 0b 00     mov #11,    r15 ;#0x000b
  52:   30 41           ret         
  54:   3f 40 07 00     mov #7, r15 ;#0x0007
  58:   30 41    

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

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

Ответ 4

Да, когда команда if терпит неудачу, в этом случае мы делаем at most 4 comparisons (Operations) во второй инструкции, а для первой команды всегда делаем 4 operations.

Изменить: Объяснение

Вторая команда if всегда быстрее первой:

Предположим, что: a = 1, b = 2, c = 0 и d = 4, в этом случае:

  • Для первой инструкции: у нас есть 3 умножения и сравнение = 4 операции

  • Для второй инструкции: мы сравниваем a с 0 (результат KO), затем b с 0 (опять KO) и c с 0 (OK) = 3 операции.

Это простая программа, которая выводит время выполнения для этих двух инструкций, вы можете изменить a, b, c и d и передать номер инструкции в качестве аргумента.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/* This is a test program to demonstrate that the second if is faster always than the first one*/
int main(int argc, char **argv)
{
        int i;
        int a = 1;
        int b = 2;
        int c = 0;
        int d = 4;
        int instruction_number;
        clock_t begin, end;
        double time_spent;

        begin = clock();

        if (argc != 2)
        {
                fprintf(stderr, "Usage : ./a.out if_instruction_number (1 or 2)\n");

                exit(EXIT_FAILURE);
        }

        instruction_number = atoi(argv[1]);

        for (i = 1; i < 100000; i++)
        {
                switch (instruction_number)
                {
                        case 1:
                                fprintf(stdout, "First if instruction : \n");
                                if (a * b * c * d == 0)
                                        fprintf(stdout, "1st instruction\n");
                                break;
                        case 2:
                                fprintf(stdout, "Second if instruction : \n");
                                if (a == 0 || b == 0 || c == 0 || d == 0)
                                        fprintf(stdout, "2nd instruction\n");
                                break;
                        default:
                                break;
                }
        }
        end = clock();
        time_spent = (double)(end - begin) / CLOCKS_PER_SEC;
        fprintf(stdout, "Time to accomplish %d instruction ---> %f\n", instruction_number, time_spent);

        return 0;
} 

Надеюсь на эту помощь.

С уважением.

Ответ 5

if ( a * b * c * d == 0 ) компилируется (без оптимизации)

movl   16(%esp), %eax
imull  20(%esp), %eax
imull  24(%esp), %eax
imull  28(%esp), %eax
testl  %eax, %eax
jne .L3

и if (a == 0 || b == 0 || c == 0 || d == 0) компилируется в

cmpl   $0, 16(%esp)
je  .L2
cmpl    $0, 20(%esp)
je  .L2
cmpl    $0, 24(%esp)
je .L2
cmpl    $0, 28(%esp)
jne .L4