优化:内联或宏函数

Optimizing: Inline or Macrofunction?

本文关键字:函数 优化      更新时间:2023-10-16

我需要尽可能优化程序。现在我遇到了这个问题:我有一个一维数组,它以像素数据的形式表示纹理。我现在需要处理这些数据。通过以下功能访问阵列:

(y * width) + x

以具有x、y坐标。现在的问题是,这个功能的优化方式是什么,我考虑了以下两种可能性:

内联:

inline int Coords(x,y) { return (y * width) + x); }

宏:

#define COORDS(X,Y) ((Y)*width)+(X)

哪一个是这里使用的最佳实践,或者有没有一种方法可以获得我不知道的更优化的变体?

我写了一个小测试程序来看看这两种方法之间的区别。

这是:

#include <cstdint>
#include <algorithm>
#include <iterator>
#include <iostream>
using namespace std;
static constexpr int width = 100;
inline int Coords(int x, int y) { return (y * width) + x; }
#define COORDS(X,Y) ((Y)*width)+(X)
void fill1(uint8_t* bytes, int height)
{
    for (int x = 0 ; x < width ; ++x) {
        for (int y = 0 ; y < height ; ++y) {
            bytes[Coords(x,y)] = 0;
        }
    }
}
void fill2(uint8_t* bytes, int height)
{
    for (int x = 0 ; x < width ; ++x) {
        for (int y = 0 ; y < height ; ++y) {
            bytes[COORDS(x,y)] = 0;
        }
    }
}
auto main() -> int
{
    uint8_t buf1[100 * 100];
    uint8_t buf2[100 * 100];
    fill1(buf1, 100);
    fill2(buf2, 100);
    // these are here to prevent the compiler from optimising away all the above code.
    copy(begin(buf1), end(buf1), ostream_iterator<char>(cout));
    copy(begin(buf2), end(buf2), ostream_iterator<char>(cout));
    return 0;
}

我是这样编译的:

c++ -S -o intent.s -std=c++1y -O3 intent.cpp

然后查看源代码,看看编译器会做什么

正如预期的那样,编译器完全忽略了程序员的所有优化尝试,而只关注别名的表达意图、副作用和可能性。然后,它为两个函数(当然是内联的)发出完全相同的代码。

组件的相关部件:

    .globl  _main
    .align  4, 0x90
_main:                                  ## @main
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp16:
    .cfi_def_cfa_offset 16
Ltmp17:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp18:
    .cfi_def_cfa_register %rbp
    pushq   %r15
    pushq   %r14
    pushq   %r13
    pushq   %r12
    pushq   %rbx
    subq    $20024, %rsp            ## imm = 0x4E38
Ltmp19:
    .cfi_offset %rbx, -56
Ltmp20:
    .cfi_offset %r12, -48
Ltmp21:
    .cfi_offset %r13, -40
Ltmp22:
    .cfi_offset %r14, -32
Ltmp23:
    .cfi_offset %r15, -24
    movq    ___stack_chk_guard@GOTPCREL(%rip), %r15
    movq    (%r15), %r15
    movq    %r15, -48(%rbp)
    xorl    %eax, %eax
    xorl    %ecx, %ecx
    .align  4, 0x90
LBB2_1:                                 ## %.lr.ph.us.i
                                        ## =>This Loop Header: Depth=1
                                        ##     Child Loop BB2_2 Depth 2
    leaq    -10048(%rbp,%rcx), %rdx
    movl    $400, %esi              ## imm = 0x190
    .align  4, 0x90
LBB2_2:                                 ##   Parent Loop BB2_1 Depth=1
                                        ## =>  This Inner Loop Header: Depth=2
    movb    $0, -400(%rdx,%rsi)
    movb    $0, -300(%rdx,%rsi)
    movb    $0, -200(%rdx,%rsi)
    movb    $0, -100(%rdx,%rsi)
    movb    $0, (%rdx,%rsi)
    addq    $500, %rsi              ## imm = 0x1F4
    cmpq    $10400, %rsi            ## imm = 0x28A0
    jne LBB2_2
## BB#3:                                ##   in Loop: Header=BB2_1 Depth=1
    incq    %rcx
    cmpq    $100, %rcx
    jne LBB2_1
## BB#4:
    xorl    %r13d, %r13d
    .align  4, 0x90
LBB2_5:                                 ## %.lr.ph.us.i10
                                        ## =>This Loop Header: Depth=1
                                        ##     Child Loop BB2_6 Depth 2
    leaq    -20048(%rbp,%rax), %rcx
    movl    $400, %edx              ## imm = 0x190
    .align  4, 0x90
LBB2_6:                                 ##   Parent Loop BB2_5 Depth=1
                                        ## =>  This Inner Loop Header: Depth=2
    movb    $0, -400(%rcx,%rdx)
    movb    $0, -300(%rcx,%rdx)
    movb    $0, -200(%rcx,%rdx)
    movb    $0, -100(%rcx,%rdx)
    movb    $0, (%rcx,%rdx)
    addq    $500, %rdx              ## imm = 0x1F4
    cmpq    $10400, %rdx            ## imm = 0x28A0
    jne LBB2_6
## BB#7:                                ##   in Loop: Header=BB2_5 Depth=1
    incq    %rax
    cmpq    $100, %rax
    jne LBB2_5
## BB#8:
    movq    __ZNSt3__14coutE@GOTPCREL(%rip), %r14
    leaq    -20049(%rbp), %r12
    xorl    %ebx, %ebx
    .align  4, 0x90
LBB2_9:                                 ## %_ZNSt3__116ostream_iteratorIccNS_11char_traitsIcEEEaSERKc.exit.us.i.i13
                                        ## =>This Inner Loop Header: Depth=1
    movb    -10048(%rbp,%r13), %al
    movb    %al, -20049(%rbp)
    movl    $1, %edx
    movq    %r14, %rdi
    movq    %r12, %rsi
    callq   __ZNSt3__124__put_character_sequenceIcNS_11char_traitsIcEEEERNS_13basic_ostreamIT_T0_EES7_PKS4_m
    incq    %r13
    cmpq    $10000, %r13            ## imm = 0x2710
    jne LBB2_9
## BB#10:
    movq    __ZNSt3__14coutE@GOTPCREL(%rip), %r14
    leaq    -20049(%rbp), %r12
    .align  4, 0x90
LBB2_11:                                ## %_ZNSt3__116ostream_iteratorIccNS_11char_traitsIcEEEaSERKc.exit.us.i.i
                                        ## =>This Inner Loop Header: Depth=1
    movb    -20048(%rbp,%rbx), %al
    movb    %al, -20049(%rbp)
    movl    $1, %edx
    movq    %r14, %rdi
    movq    %r12, %rsi
    callq   __ZNSt3__124__put_character_sequenceIcNS_11char_traitsIcEEEERNS_13basic_ostreamIT_T0_EES7_PKS4_m
    incq    %rbx
    cmpq    $10000, %rbx            ## imm = 0x2710
    jne LBB2_11
## BB#12:                               ## %_ZNSt3__14copyIPhNS_16ostream_iteratorIccNS_11char_traitsIcEEEEEET0_T_S7_S6_.exit
    cmpq    -48(%rbp), %r15
    jne LBB2_14
## BB#13:                               ## %_ZNSt3__14copyIPhNS_16ostream_iteratorIccNS_11char_traitsIcEEEEEET0_T_S7_S6_.exit
    xorl    %eax, %eax
    addq    $20024, %rsp            ## imm = 0x4E38
    popq    %rbx
    popq    %r12
    popq    %r13
    popq    %r14
    popq    %r15
    popq    %rbp
    retq

注意,如果没有复制的两个调用(…,ostream_iterator…),编译器推测程序的总体效果为零,并拒绝发出任何代码,除了从main() 返回0

这个故事的寓意是:不要再做编译器的工作了。继续你的。

你的工作是尽可能优雅地表达意图。仅此而已。

内联函数,原因有两个:

  • 它不太容易出现错误
  • 它让编译器决定是否内联,这样您就不必浪费时间担心这些琐碎的事情

第一项工作:修复宏中的错误。

如果您担心这一点,请使用编译器指令实现这两种方法,并对结果进行评测。

inline int Coords(x,y)更改为inline int Coords(const x, const y),因此,如果宏版本确实运行得更快,则如果对宏进行重构以修改参数,则inline内部版本将出错。

我的预感是,在一个良好的优化构建中,该功能不会比宏慢。没有宏的代码库更容易维护。

如果你最终选择了宏,那么为了程序的稳定性,我也倾向于将width作为宏参数传递。

我很惊讶没有人提到函数和宏之间的一个主要区别:任何编译器都可以内联函数,但没有多少编译器(如果有的话)可以从宏中创建函数,即使这会提高性能。

我会给出一个不同的答案,因为这个问题似乎着眼于错误的解决方案。它比较了两件事,即使是90年代(甚至80年代)最基本的优化器也应该能够优化到相同的程度(一个微不足道的单行函数与一个宏)。

如果你想在这里提高性能,你必须在编译器优化的不那么琐碎的解决方案之间进行比较。

例如,假设您以顺序方式访问纹理。然后,您不需要通过(y*w) + x访问像素,只需依次迭代即可:

for (int j=0; j < num_pixels; ++j)
    // do something with pixels[j]

在实践中,我看到了这类循环相对于y/x双循环的性能优势,即使是在最现代的编译器中也是如此。

假设您没有完全按顺序访问事物,但仍然可以访问扫描线内的相邻水平像素。在这种情况下,您可以通过执行以下操作来提高性能:

// Given a particular y value:
Pixel* scanline = pixels + y*w;
for (int x=0; x < w; ++x)
    // do something with scanline[x]

如果你没有做这两件事,并且需要对图像进行完全随机访问,也许你可以找到一种方法,使你的内存访问模式更加统一(在驱逐之前访问可能在同一L1缓存行中的更多水平像素)。

有时,如果转置图像会导致后续内存访问的大部分在扫描线内水平,而不是跨扫描线(由于空间局部性),那么转置图像的成本甚至是值得的。转换图像的成本(基本上是将图像旋转90度并将行与列交换)将远远弥补之后访问图像的成本降低,这可能看起来很疯狂,但以高效、缓存友好的模式访问内存是一笔巨大的交易,尤其是在图像处理中(比如每秒数亿像素与每秒仅数百万像素之间的差异)。

如果你不能做到这一切,仍然需要随机访问,并且你在这里面临探查器热点,然后,将纹理图像分割成更小的瓦片可能会有所帮助(这意味着渲染更多纹理的四边形/三角形,甚至可能做额外的工作来确保每个纹理瓦片边界处的无缝结果,但它可以平衡,如果处理纹理的开销,额外的几何体开销可能会超过成本)。这将增加引用的局部性,并通过实际减少以完全随机访问的方式处理的纹理输入的大小,在驱逐之前将更多缓存的内存用于更快但更小的内存的可能性。

这些技术中的任何一种都可以提高性能——试图通过使用宏来优化一个单行函数,除了让代码更难维护之外,几乎没有任何帮助。在最好的情况下,宏可能会在完全未优化的调试构建中提高性能,但这会违背调试构建的全部目的,因为调试构建本来很容易调试,而且宏很难调试。