Каков наилучший способ (с точки зрения производительности) проверить, находится ли значение в пределах порога? - PullRequest
1 голос
/ 08 мая 2011

То есть, какой самый быстрый способ сделать тест

if( a >= ( b - c > 0 ? b - c : 0 ) &&
    a <= ( b + c < 255 ? b + c : 255 ) )

    ...

, если a, b и c все unsigned char aka BYTE.Я пытаюсь оптимизировать процесс сканирования изображения, чтобы найти подизображение, и такое сравнение выполняется примерно 3 миллиона раз за одно сканирование, поэтому даже незначительные оптимизации могут быть полезны.

Не уверен, но, возможно,какая-то побитовая операция?Может быть, добавление 1 к c и тестирование на меньше или больше, чем без или равной части?Я не знаю!

Ответы [ 4 ]

4 голосов
/ 08 мая 2011

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

a >= b - c
a <= b + c
subtract b from both:
a - b >= -c
a - b <= c

Теперь это равно

abs(a - b) <= c

И вcode:

(a>b ? a-b : b-a) <= c

Теперь этот код немного быстрее и не содержит (или не требует) сложных проверок недостаточного / переполнения.


Я профилировал мой и 6502-екод с 1000000000 повторений и официально без разницы .Я бы предложил выбрать самое элегантное решение (которое принадлежит IMO, но мнения расходятся), поскольку производительность не является аргументом.


Однако между кодом my и asker была заметная разница.Вот код профилирования, который я использовал:

#include <iostream>

int main(int argc, char *argv[]) {  
    bool prevent_opti;
    for (int ai = 0; ai < 256; ++ai) {
        for (int bi = 0; bi < 256; ++bi) {
            for (int ci = 0; ci < 256; ++ci) {
                unsigned char a = ai;
                unsigned char b = bi;
                unsigned char c = ci;
                if ((a>b ? a-b : b-a) <= c) prevent_opti = true;
            }
        }
    }

    std::cout << prevent_opti << "\n";

    return 0;
}

В моем операторе if это заняло в среднем 120 мс, а в операторе спрашивающего - 135 мс.

2 голосов
/ 08 мая 2011

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

Мои предпочтения будут:

int min = (b-c) > 0  ? (b-c) : 0 ;
int max = (b+c) < 255? (b+c) : 255;

if ((a >= min) && ( a<= max))

Оригинальный код: (в сборке)

movl    %eax, %ecx
movl    %ebx, %eax
subl    %ecx, %eax
movl    $0, %edx
cmovs   %edx, %eax
cmpl    %eax, %r12d
jl  L13
leal    (%rcx,%rbx), %eax
cmpl    $255, %eax
movb    $-1, %dl
cmovg   %edx, %eax
cmpl    %eax, %r12d
jmp L13

Мой код (на ассамблее)

movl    %eax, %ecx
movl    %ebx, %eax
subl    %ecx, %eax
movl    $0, %edx
cmovs   %edx, %eax
cmpl    %eax, %r12d
jl  L13
leal    (%rcx,%rbx), %eax
cmpl    $255, %eax
movb    $-1, %dl
cmovg   %edx, %eax
cmpl    %eax, %r12d
jg  L13

код ночного взломщика (на ассамблее)

movl    %r12d, %edx
subl    %ebx, %edx
movl    %ebx, %ecx
subl    %r12d, %ecx
cmpl    %ebx, %r12d
cmovle  %ecx, %edx
cmpl    %eax, %edx
jg  L16
2 голосов
/ 08 мая 2011

Просто использование обычных int s для a, b и c позволит вам изменить код на более простой

if (a >= b - c && a <= b + c) ...

Также, в качестве альтернативы, 256 * 256 * 256 - это просто 16 МБ, а карта из 16 Мбит - 2 МБ. Это означает, что возможно использовать справочную таблицу типа

int index = (a<<16) + (b<<8) + c;
if (lookup_table[index>>3] & (1<<(index&7))) ...

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

Другая альтернатива - использовать немного алгебры

b - c <= a <= b + c
      iff
- c <= a - b <= c        (subtracted b from all terms)
      iff
0 <= a - b + c <= 2*c    (added c to all terms)

это позволяет использовать только один тест

if ((unsigned)(a - b + c) < 2*c) ...

при условии, что a, b и c равны int с. Причина в том, что если a - b + c отрицательно, то арифметика без знака сделает его намного больше, чем 2*c (если c равно 0..255). Это должно генерировать эффективный машинный код с одной ветвью, если процессор имеет специальные инструкции сравнения со знаком / без знака, такие как x86 (ja / jg).

#include <stdio.h>

int main()
{
    int err = 0;

    for (int ia=0; ia<256; ia++)
        for (int ib=0; ib<256; ib++)
            for (int ic=0; ic<256; ic++)
            {
                unsigned char a = ia;
                unsigned char b = ib;
                unsigned char c = ic;
                int res1 = (a >= ( b - c > 0 ? b - c : 0 ) &&
                            a <= ( b + c < 255 ? b + c : 255 ));
                int res2 = (unsigned(a - b + c) <= 2*c);

                err += (res1 != res2);
            }
    printf("Errors = %i\n", err);
    return 0;
}

На x86 с g ++ код ассемблера, сгенерированный для теста res2, содержит только одну условную инструкцию.

Код ассемблера для следующего цикла:

void process(unsigned char *src, unsigned char *dst, int sz)
{
    for (int i=0; i<sz; i+=3)
    {
        unsigned char a = src[i];
        unsigned char b = src[i+1];
        unsigned char c = src[i+2];
        dst[i] = (unsigned(a - b + c) <= 2*c);
    }
}


.L3:
    movzbl  2(%ecx,%eax), %ebx    ; This loads c
    movzbl  (%ecx,%eax), %edx     ; This loads a
    movzbl  1(%ecx,%eax), %esi    ; This loads b
    leal    (%ebx,%edx), %edx     ; This computes a + c
    addl    %ebx, %ebx            ; This is c * 2
    subl    %esi, %edx            ; This is a - b + c
    cmpl    %ebx, %edx            ; Comparison
    setbe   (%edi,%eax)           ; Set 0/1 depending on result
    addl    $3, %eax              ; next group
    cmpl    %eax, 16(%ebp)        ; did we finish ?
    jg  .L3                   ; if not loop back for next

Вместо этого dst[i] = (a<b ? b-a : a-b); код становится намного длиннее

.L9:
    movzbl  %dl, %edx
    andl    $255, %esi
    subl    %esi, %edx
.L4:
    andl    $255, %edi
    cmpl    %edi, %edx
    movl    12(%ebp), %edx
    setle   (%edx,%eax)
    addl    $3, %eax
    cmpl    %eax, 16(%ebp)
    jle .L6
.L5:
    movzbl  (%ecx,%eax), %edx
    movb    %dl, -13(%ebp)
    movzbl  1(%ecx,%eax), %esi
    movzbl  2(%ecx,%eax), %edi
    movl    %esi, %ebx
    cmpb    %bl, %dl
    ja  .L9
    movl    %esi, %ebx
    movzbl  %bl, %edx
    movzbl  -13(%ebp), %ebx
    subl    %ebx, %edx
    jmp .L4
    .p2align 4,,7
    .p2align 3
.L6:

И я слишком устал, чтобы пытаться расшифровать его (2:28 утра здесь)

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

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

Например, Linux во время начальной загрузки выполняет тест, чтобы определить, какой более быстрый способ выполнить определенные вычисления, необходимые для ядра. Переменных слишком много (размер / уровни кэша, скорость оперативной памяти, тактовая частота процессора, набор микросхем, тип процессора ...).

1 голос
/ 08 мая 2011

Редко встраивание троичного оператора в другой оператор улучшает производительность:)

Если каждый код операции имеет значение, напишите коды операций самостоятельно - используйте ассемблер. Также рассмотрите возможность использования инструкций simd, если это возможно. Я также был бы заинтересован в целевой платформе. Ассемблер ARM любит сравнения такого рода и имеет коды операций для ускорения насыщенной математики этого типа.

...