C++ 优化:内联还是宏函数?
我需要尽可能地优化一个程序。现在我遇到了这个问题:我有一个一维数组,它以像素数据的形式表示纹理。我现在需要处理这些数据。可通过以下功能访问阵列:C++ 优化:内联还是宏函数?,c++,arrays,optimization,C++,Arrays,Optimization,我需要尽可能地优化一个程序。现在我遇到了这个问题:我有一个一维数组,它以像素数据的形式表示纹理。我现在需要处理这些数据。可通过以下功能访问阵列: (y * width) + x 有x,y坐标。现在的问题是,对于该功能,哪种方式最为优化,我考虑了以下两种可能性: 内联: inline int Coords(x,y) { return (y * width) + x); } 宏: #define COORDS(X,Y) ((Y)*width)+(X) 在这里使用哪一个是最佳实践,或者有没有一种
(y * width) + x
有x,y坐标。现在的问题是,对于该功能,哪种方式最为优化,我考虑了以下两种可能性:
内联:
inline int Coords(x,y) { return (y * width) + x); }
宏:
#define COORDS(X,Y) ((Y)*width)+(X)
在这里使用哪一个是最佳实践,或者有没有一种方法可以获得我不知道的更优化的变体?内联函数,原因有两个: 它不太容易出现虫子, 它让编译器决定是否内联,因此您不必浪费时间担心这些琐碎的事情。
第一项工作:修复宏中的错误 如果您担心这个问题,请使用编译器指令实现这两种方法并分析结果 将内联int-Coordsx,y更改为内联int-Coordsconst x,const y,因此,如果宏版本的运行速度更快,那么如果对宏进行重构以修改参数,则内联生成版本将出错 我的直觉是,在一个良好的优化构建中,函数不会比宏慢。没有宏的代码库更容易维护
如果你最终决定使用宏,那么为了程序的稳定性,我也倾向于将width作为宏参数传递。我编写了一个小测试程序,看看这两种方法之间的区别 这是:
#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;
}
然后查看源代码,看看编译器会做什么
正如预期的那样,编译器完全忽略了程序员进行优化的所有尝试,而只关注表达的意图、副作用和别名的可能性。然后,它为两个函数发出完全相同的代码,这两个函数当然是内联的
组件的相关部分:
.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_迭代器。。。编译器推测程序的总体效果是无效的,并且拒绝发出任何代码,除了从main返回0之外
这个故事的寓意是:停止尝试做编译器的工作。继续你的
你的工作就是尽可能优雅地表达你的意图。仅此而已。我很惊讶没有人提到函数和宏之间的一个主要区别:任何编译器都可以内联函数,但是,即使有利于性能,也没有多少人能够从宏中创建函数。我会给出一个不同的答案,因为这个问题似乎在寻找错误的解决方案。它比较了两件事,即使是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]
如果您不执行这两项操作,并且需要完全随机访问图像,那么您可以找到一种方法,使内存访问模式更加统一,在逐出之前访问可能位于同一个一级缓存线中的更多水平像素
有时,如果由于空间位置的原因,导致后续内存访问的大部分在扫描线内是水平的,而不是跨扫描线,那么转置图像的成本甚至是值得的。将图像旋转90度并将行与列交换所需的转置成本将远远弥补随后访问图像所需的降低成本,这似乎有些疯狂,但以高效、缓存友好的模式访问内存是一件大事,特别是在图像处理方面,比如每秒数亿像素与每秒仅数百万像素之间的差异
如果您无法执行任何操作,并且仍然需要随机访问,并且您在这里面临探查器热点,那么将纹理图像分割为更小的分片可能会有所帮助,这意味着渲染更多纹理四边形/三角形,甚至可能做额外的工作,以确保在每个纹理分片的边界处获得无缝结果,但是,如果您的开销是在处理纹理,那么它可以平衡,并且额外的几何体开销可以超过成本。这将增加引用的局部性,并增加使用更多缓存内存以更快但更小内存pri的可能性
或者通过实际减少纹理输入的大小,以完全随机访问的方式进行逐出
这些技术中的任何一种都可以提高性能——试图通过使用宏来优化单行函数,除了使代码更难维护之外,几乎没有任何帮助。在最佳情况下,宏可能会在完全未优化的调试版本中提高性能,但这种情况违背了调试版本的全部目的,即易于调试,并且宏是出了名的难以调试。鉴于宏的实现不正确,例如,尝试使用1+2表示y,我会使用这个函数。就我个人而言,我讨厌宏引用的变量/对象不是宏宽度的局部变量/对象,因为这会带来麻烦,除非宽度是一个常数,否则它可能没问题。这是一个常见的误解,即编译器产生的机器代码会因您尝试过早优化源代码而得到改进。编写清晰、安全的代码,表达意图。相信编译器会完成它的工作。@BarmakShemirani不要散布混乱。宏并没有传递更少的数据。它也没有占用更多的空间。编译器会根据需要编译这两个文件。出于显而易见的原因,大多数编译器都喜欢快速代码。您关于评测的提示是正确的,但在本例中肯定不是required@BarmakShemirani让我更清楚一点。宏不可能更快,因为它不传递数据。期间。嗯。。。我喜欢const部分,但是使用const int&x不是更快吗?我希望OP中的bug现在已经修复了。const int&:我非常怀疑。int通过值传递的速度非常快,因为它通常是系统上的本机整数类型。回答太棒了!非常感谢。虽然我同意你的结论,但是你的测试是不必要的复杂:为了防止编译器优化你的代码,你需要做的就是使用命令行参数来计算Coord或Coord并返回它。生成的程序集是相同的:一个imul和一个add。@VladFeinstein好吧,我想有很多方法可以剥猫皮。我希望用这个演示来说明汇编程序的输出与C++源代码完全不同。从这个意义上讲,它比我预期的工作得更好,因为编译器优化了2级函数调用,展开了2个循环,并构建了一种高效的缓存方式来清零数组。我认为这一课对于那些错误地认为早期优化有任何好处的年轻程序员来说是很重要的。我喜欢这个答案——我想这是为了幽默,你可能提前知道结论?无论如何,这类问题的完美答案。@Ike很高兴你喜欢它。写起来很有趣:我认为用宏创建函数是不可能的,因为编译器从来都看不到宏,预编译器在编译器完成它的工作之前就完成了…@Nidhoegger-你是同意还是反对我的观点是-给编译器一个选择,它会比你更清楚如何处理函数。我猜两者都有一点。我同意你的论点,但也争论了,但不是很多部分,因为我猜这对编译器来说是不可能的,他需要一些逻辑来确定这个代码部分在源代码中是重复的,并将其外包给一个函数。我可以对你的论点进行编辑吗?请删除“平凡”一词,因为它不是:
// Given a particular y value:
Pixel* scanline = pixels + y*w;
for (int x=0; x < w; ++x)
// do something with scanline[x]