【问题标题】:What is the best way (performance-wise) to test whether a value falls within a threshold?测试值是否在阈值内的最佳方法(性能方面)是什么?
【发布时间】:2011-09-12 00:52:19
【问题描述】:

也就是说,最快的测试方法是什么

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

    ...

如果 a、b 和 c 都是 unsigned char aka BYTE。我正在尝试优化图像扫描过程以找到子图像,并且每次扫描会进行大约 300 万次这样的比较,因此即使是很小的优化也可能会有所帮助。

不确定,但也许是某种按位运算?也许将 1 添加到 c 并在没有或等于部分的情况下测试小于和大于?我不知道!

【问题讨论】:

  • 你的意思是a
  • 不,我的意思是 a 在 b 的 c 个单位内,( a >= b - c AND a
  • 一般c比较小,a和b随便。 a 和 b 代表 R、G 或 B 像素值,c 是它们必须在“匹配”范围内的可接受阈值。我有这个阈值来解释阴影非常略微不同并且仅以像素值的小间隔关闭的等效图像。

标签: c++ performance optimization byte


【解决方案1】:

好吧,首先让我们看看你要检查什么而不进行各种上溢/下溢检查:

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

现在等于

abs(a - b) <= c

在代码中:

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

现在,此代码稍微快了一点,并且不包含(或不需要)复杂的下溢/上溢检查。


我已经用 1000000000 次重复分析了我的和 6502 的代码,并且正式 没有任何区别。我建议选择最优雅的解决方案(这是我的 IMO,但意见不同),因为性能不是一个论据。


但是,我的代码和提问者的代码之间存在显着差异。这是我使用的分析代码:

#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 毫秒,而提问者的 if 语句平均耗时 135 毫秒。

【讨论】:

  • @Jason D:偷别人的风头是什么意思?英语不是我的母语。但我想这是非常积极的事情:)
  • 美丽。我知道周围潜伏着这样的东西。谢谢。
  • 这仍然使用两个条件(一个用于三元运算符,一个用于与c比较)
  • @6502:这些条件语句的速度非常快!
  • @Martin:除了 a 不能为负数,因为它是无符号的。
【解决方案2】:

它认为通过以最清晰的方式编写它然后打开编译器优化器,您将获得最佳性能。编译器非常擅长这种优化,并且在大多数情况下会打败你(在最坏的情况下它会和你一样)。

我的偏好是:

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

nightcracker 的代码(汇编中)

movl    %r12d, %edx
subl    %ebx, %edx
movl    %ebx, %ecx
subl    %r12d, %ecx
cmpl    %ebx, %r12d
cmovle  %ecx, %edx
cmpl    %eax, %edx
jg  L16

【讨论】:

  • 对不起,马丁,但您的偏好需要我 20 秒才能掌握。我认为我的(a&gt;b ? a-b : b-a) &lt;= c 更快更容易阅读。如果不是,那么我会把这个评论放在后面:// abs(a - b) &lt;= c.
  • 有趣的方法,使用冗长以便编译器确切地知道你在做什么,然后可以使用它的聪明技巧来做到最好。仍然使用这么多操作让我感到紧张。我真的应该编译所有这些,看看哪个有最好的汇编器。
  • @night:我认为我的更容易阅读。很难理解您正在检查 a 是否必须在范围内。至于更高效,你的指令少了 5 条(使用 -O3)。
  • @rotanimod:详细程度不适用于编译器。这是给维护者的。编译器擅长把你弄得一团糟。
  • 考虑到差异,我认为您会发现实际程序中的速度没有差异,因为其余代码中的任何处理器停顿都会完全消除任何优势。因此,可读性和可维护性变得很重要。
【解决方案3】:

只需对abc 使用普通的ints 即可将代码更改为更简单的代码

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

另外,作为替代方案,256*256*256 仅为 16M,而 16M 位的映射为 2 MBytes。这意味着使用类似的查找表是可行的

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) ...

假设abc 是普通的ints。原因是如果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;
}

在使用 g++ 的 x86 上,为 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&lt;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 在引导期间会进行 ae 测试,以确定执行内核所需的特定计算的更快方法。变量太多(缓存大小/级别、内存速度、cpu 时钟、芯片组、cpu 类型...)。

【讨论】:

  • @6502:当问题明确指出unsigned char时,你怎么能假设a, b, cints。
  • 不过,我需要使用 unsigned char,因为使用 int 会使我的所有图像对象的大小翻两番,这也不好。我喜欢你的第二个选项的外观......但我必须承认我不完全理解第二行在做什么。
  • 如果abc 是变量,您可以将它们放在int 中,但图像中仍然包含unsigned chars。 charshortfloat 顺便说一下是针对 C++ 的“存储类型”:它们在每个表达式中自动转换为 int/doubles(即 int x = a + b;ab 是 @值为 255 的 987654355@s 给出 510 ...换句话说,即使它是无符号字符之间的总和,也没有换行。
  • 好的,所以我的理解是if ((unsigned)(a - b + c) &lt; 2*c) ... 无论如何都应该工作?
  • 看起来很不错。您能否发布 res2 语句的完整汇编程序转储?我只是看到我的初始代码有 13 个 asm 语句,nightcrawler 有 8 个,而你的有一个未知数,但肯定少了一个。所以这似乎是一个可能的赢家。
【解决方案4】:

在另一个语句中嵌入三元运算符很少能提高性能:)

如果每个操作码都很重要,请自己编写操作码 - 使用汇编程序。如果可能,还可以考虑使用 simd 指令。我也会对目标平台感兴趣。 ARM 汇编器喜欢这种比较,并且有操作码来加速这种类型的饱和数学。

【讨论】:

  • 我的汇编经验极少。我认为我必须在 C++ 中做到最好,然后信任编译器。但是说得好,我应该考虑更深入地挖掘一些性能巨大的东西,这就是其中之一。
猜你喜欢
  • 2016-05-23
  • 1970-01-01
  • 2019-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-15
  • 2020-12-25
相关资源
最近更新 更多