【问题标题】:Optimizing: Inline or Macrofunction?优化:内联还是宏功能?
【发布时间】:2015-11-12 18:51:02
【问题描述】:

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

(y * width) + x

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

内联:

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

宏:

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

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

【问题讨论】:

  • 鉴于您的宏实现不正确(例如,尝试使用 1+2 表示 y),我会使用该函数。
  • 就我个人而言,我讨厌宏引用不是宏本地的变量/对象(宽度),因为这会带来麻烦(除非宽度是一个常数,那么它可能没问题)。
  • 一种常见的误解是,编译器产生的机器代码会因您过早地优化源代码而得到改进。编写清晰、安全的代码来表达意图。相信编译器会完成它的工作。
  • @BarmakShemirani 不要散布混乱。宏不是“传递更少的数据”。它也不是“使用更多空间”。编译器会按照它认为合适的方式编译两者。大多数编译器偏爱快速代码(原因很明显)。您关于分析的提示是正确的,尽管在这种情况下它肯定不是必需的
  • @BarmakShemirani 让我更清楚。宏不可能“更快,因为它不传递数据”。期间。

标签: c++ arrays optimization


【解决方案1】:

我写了一个小测试程序来看看这两种方法有什么区别。

这里是:

#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

请注意,如果没有对 copy(..., ostream_iterator...) 的两次调用,编译器会推测程序的总体效果是什么,并拒绝发出任何代码,除了从 @987654324 返回 0 @

故事的寓意:停止尝试完成编译器的工作。继续你的。

您的工作是尽可能优雅地表达意图。就是这样。

【讨论】:

  • 很棒的答案!谢谢!
  • 虽然我同意你的结论,但你的测试是不必要的复杂:为了防止编译器优化你的代码,你需要做的就是使用命令行参数来计算 Coord() 或 COORD()并返回它。生成的程序集是相同的:一个 imul 和一个 add。
  • @VladFeinstein 好吧,我想有很多方法可以给猫剥皮。我希望通过这个演示展示的是汇编程序输出 c++ 源代码完全不同。从这个意义上说,它比我预期的要好,因为编译器优化了 2 级函数调用,展开了 2 个循环并构建了一种缓存有效的方法来清零数组。我认为对于那些错误地认为早期优化有任何好处的年轻程序员来说,这一课很重要。
  • 我喜欢这个答案——我假设它有点幽默,你可能事先知道结论?无论如何,这类问题的完美答案。
  • @Ike 很高兴你喜欢它。写起来很有趣:)
【解决方案2】:

内联函数,有两个原因:

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

【讨论】:

  • 我可以对您的论点进行修改吗?请删除“琐碎”一词,因为它不是:)
【解决方案3】:

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

如果您担心,请使用编译器指令实现这两种方式并分析结果。

inline int Coords(x,y) 更改为inline int Coords(const x, const y) 因此,如果宏版本确实更快,那么如果宏被重构以修改参数,inline 构建版本将出错。

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

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

【讨论】:

  • 嗯...我喜欢const 部分,但使用const int &amp; x 会不会更快?
  • 我希望OP中的错误现在已经修复。
  • const int&amp;:我非常怀疑。 int 通过值传递非常快,因为它通常是系统上的“本机”整数类型。
【解决方案4】:

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

【讨论】:

  • 我认为不可能从宏中创建一个函数,因为编译器永远不会“看到”宏,预编译器在编译器完成它之前完成它的工作......
  • @Nidhoegger - 你同意还是争论? :) 我的意思是 - 给编译器一个选择,它会比你更清楚如何处理函数。
  • 我猜两者兼而有之。我同意您的论点,但也对“但不是很多”部分提出异议,因为我猜这对编译器来说是不可能的(他需要一些逻辑来确定此代码部分在源代码中重复并将其外包给一个函数)
【解决方案5】:

我会提供一个不同的答案,因为这个问题似乎正在寻找错误的解决方案。它比较了两件事,即使是 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 度并将行与列交换)足以弥补之后访问它的成本降低,这似乎很疯狂,但以高效、缓存友好的模式访问内存是一种非常重要,尤其是在图像处理方面(例如每秒数亿像素与每秒数百万像素之间的差异)。

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

这些技术中的任何一种都可以提高性能——尝试通过使用宏来优化单行函数几乎不可能有任何帮助,只会使代码更难维护。在最好的情况下,宏可能会在完全未优化的调试版本中提高性能,但这违背了调试版本的全部目的,即易于调试,而且众所周知,宏很难调试。

【讨论】:

    猜你喜欢
    • 2011-10-08
    • 1970-01-01
    • 1970-01-01
    • 2012-05-08
    • 2020-05-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多