优化:内联或宏函数
Optimizing: Inline or Macrofunction?
我需要尽可能优化程序。现在我遇到了这个问题:我有一个一维数组,它以像素数据的形式表示纹理。我现在需要处理这些数据。通过以下功能访问阵列:
(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度并将行与列交换)将远远弥补之后访问图像的成本降低,这可能看起来很疯狂,但以高效、缓存友好的模式访问内存是一笔巨大的交易,尤其是在图像处理中(比如每秒数亿像素与每秒仅数百万像素之间的差异)。
如果你不能做到这一切,仍然需要随机访问,并且你在这里面临探查器热点,然后,将纹理图像分割成更小的瓦片可能会有所帮助(这意味着渲染更多纹理的四边形/三角形,甚至可能做额外的工作来确保每个纹理瓦片边界处的无缝结果,但它可以平衡,如果处理纹理的开销,额外的几何体开销可能会超过成本)。这将增加引用的局部性,并通过实际减少以完全随机访问的方式处理的纹理输入的大小,在驱逐之前将更多缓存的内存用于更快但更小的内存的可能性。
这些技术中的任何一种都可以提高性能——试图通过使用宏来优化一个单行函数,除了让代码更难维护之外,几乎没有任何帮助。在最好的情况下,宏可能会在完全未优化的调试构建中提高性能,但这会违背调试构建的全部目的,因为调试构建本来很容易调试,而且宏很难调试。
- 使用仅使用一次的变量调用的复制构造函数.这可能是通过调用move构造函数进行编译器优化的情况吗
- 纯函数,为什么没有优化
- 线性优化目标函数中的绝对值
- 这个C++编译器优化(在自身的实例上调用对象自己的构造函数)的名称是什么,它是如何工作的?
- 何时允许编译器优化复制构造函数
- C++延迟后的优化器调用函数
- 未使用的C++未优化的静态成员函数/变量
- 我应该保留这个函数来查找第 n 个素数还是可以优化?
- 如何使用 g2o 优化多约束函数
- GCC 能否优化具有相同主体的函数的代码大小?
- 对于优化级别为 0 的 std::vector,析构函数被调用两次
- 尾递归函数未被 g++ 优化
- 函数优化传递
- 优化在网格图中查找哈密尔循环的函数?
- C++将 lambda 函数另存为成员变量,而不使用函数指针进行优化
- 使用谷神星优化多维函数
- 创建一个始终返回零但优化器不知道的函数
- C++生成器 10.2 基于函数的优化状态"unknown attribute 'optimize' ignored"
- 使用函数优化数组中的代码最大元素
- C++11移动构造函数优化