访问硬编码数组和 运行 时间初始化数组之间有什么性能差异吗?
Is there any performance difference between accessing a hardcode array and a run time initialization array?
例如,我想使用数组SQRT[i]创建一个平方根table来优化一个游戏,但是我不知道在访问SQRT[i] 的值:
硬编码数组
int SQRT[]={0,1,1,1,2,2,2,2,2,3,3,.......255,255,255}
在 运行 时间生成值
int SQRT[65536];
int main(){
for(int i=0;i<65536;i++){
SQRT[i]=sqrt(i);
}
//other code
return 0;
}
一些访问它们的例子:
if(SQRT[a*a+b*b]>something)
...
我不清楚程序是否以不同的方式存储或访问硬编码数组,也不知道编译器是否会优化硬编码数组以加快访问时间,是访问数组时它们之间有性能差异吗?
访问时间相同。当您对数组进行硬编码时,在 main 之前调用的 C 库例程将初始化它(在嵌入式系统中,启动代码复制读写数据,即从 ROM 硬编码到数组所在的 RAM 地址,如果数组是常量,然后直接从 ROM 访问它)。
如果使用for循环初始化,调用Sqrt函数会有开销
首先,你应该做正确的硬编码数组:
static const int SQRT[]={0,1,1,1,2,2,2,2,2,3,3,.......255,255,255};
(也使用 uint8_t
而不是 int
可能更好,以减少数组的大小并使其对缓存更友好)
与替代方法相比,这样做有一大优势:编译器可以轻松检查数组的内容是否无法更改。
如果没有这个,编译器就必须偏执——每个函数调用都可能改变 SQRT
的内容,并且每个指针都可能指向 SQRT
,因此任何通过 int*
或 char*
可能正在修改数组。如果编译器无法证明这不会发生,那么就会限制它可以进行的优化种类,这在某些情况下可能会影响性能。
另一个潜在的优势是能够在编译时解决涉及常量的问题。
如果需要,您可以巧妙地使用 __restrict__
.
来帮助编译器解决问题
在现代 C++ 中,您可以两全其美;应该可以(并且以合理的方式)编写代码,在编译时运行初始化SQRT
作为 constexpr
。不过,最好问一个新问题。
正如人们在评论中所说:
if(SQRT[a*a+b*b]>something)
是一个可怕的示例用例。如果这就是您需要 SQRT 的全部,只需平方 something
.
只要你能告诉编译器SQRT
没有任何别名,那么一个运行-time loop会让你的executable变小,只需要加一个启动期间的少量 CPU 开销。绝对使用 uint8_t
,而不是 int
。从 8 位内存位置加载 32 位临时文件并不比从零填充的 32b 内存位置慢。 (在 x86 上额外的 movsx
指令,而不是使用内存操作数,将在减少缓存污染方面为自己付出更多的代价。RISC 机器通常不允许内存操作数,所以你总是需要一条指令来加载值存入寄存器。)
此外,sqrt
是 Sandybridge 上的 10-21 周期延迟。如果您不经常需要它,int->double、sqrt、double->int 链并不比 L2 缓存命中差多少。比进入 L3 或主内存更好。如果你需要很多 sqrt
,那么当然,做一个 LUT。吞吐量会好得多,即使您在 table 中跳来跳去并导致 L1 未命中。
你可以通过平方而不是 sqrting 来优化初始化,比如
uint8_t sqrt_lookup[65536];
void init_sqrt (void)
{
int idx = 0;
for (int i=0 ; i < 256 ; i++) {
// TODO: check that there isn't an off-by-one here
int iplus1_sqr = (i+1)*(i+1);
memset(sqrt_lookup+idx, i, iplus1_sqr-idx);
idx = iplus1_sqr;
}
}
您仍然可以获得 sqrt_lookup
为 const
的好处(编译器知道它不能别名)。要么使用 restrict
,要么对编译器撒谎,因此 table 的用户看到一个 const
数组,但您实际上写入了它。
这可能涉及欺骗编译器,在大多数地方声明它 extern const
,而不是在初始化它的文件中。您必须确保这确实有效,并且不会创建引用两个不同符号的代码。如果你只是在初始化它的函数中丢弃 const
,如果编译器将它放在 rodata
中(或者如果未初始化,则只读 bss
内存,如果这在某些平台上可能吗?)
也许我们可以避免对编译器撒谎,方法是:
uint8_t restrict private_sqrt_table[65536]; // not sure about this use of restrict, maybe that will do it?
const uint8_t *const sqrt_lookup = private_sqrt_table;
实际上,这只是一个指向 const
数据的 const
指针,不能保证它所指向的内容不会被其他引用更改。
例如,我想使用数组SQRT[i]创建一个平方根table来优化一个游戏,但是我不知道在访问SQRT[i] 的值:
硬编码数组
int SQRT[]={0,1,1,1,2,2,2,2,2,3,3,.......255,255,255}
在 运行 时间生成值
int SQRT[65536]; int main(){ for(int i=0;i<65536;i++){ SQRT[i]=sqrt(i); } //other code return 0; }
一些访问它们的例子:
if(SQRT[a*a+b*b]>something)
...
我不清楚程序是否以不同的方式存储或访问硬编码数组,也不知道编译器是否会优化硬编码数组以加快访问时间,是访问数组时它们之间有性能差异吗?
访问时间相同。当您对数组进行硬编码时,在 main 之前调用的 C 库例程将初始化它(在嵌入式系统中,启动代码复制读写数据,即从 ROM 硬编码到数组所在的 RAM 地址,如果数组是常量,然后直接从 ROM 访问它)。
如果使用for循环初始化,调用Sqrt函数会有开销
首先,你应该做正确的硬编码数组:
static const int SQRT[]={0,1,1,1,2,2,2,2,2,3,3,.......255,255,255};
(也使用 uint8_t
而不是 int
可能更好,以减少数组的大小并使其对缓存更友好)
与替代方法相比,这样做有一大优势:编译器可以轻松检查数组的内容是否无法更改。
如果没有这个,编译器就必须偏执——每个函数调用都可能改变 SQRT
的内容,并且每个指针都可能指向 SQRT
,因此任何通过 int*
或 char*
可能正在修改数组。如果编译器无法证明这不会发生,那么就会限制它可以进行的优化种类,这在某些情况下可能会影响性能。
另一个潜在的优势是能够在编译时解决涉及常量的问题。
如果需要,您可以巧妙地使用 __restrict__
.
在现代 C++ 中,您可以两全其美;应该可以(并且以合理的方式)编写代码,在编译时运行初始化SQRT
作为 constexpr
。不过,最好问一个新问题。
正如人们在评论中所说:
if(SQRT[a*a+b*b]>something)
是一个可怕的示例用例。如果这就是您需要 SQRT 的全部,只需平方 something
.
只要你能告诉编译器SQRT
没有任何别名,那么一个运行-time loop会让你的executable变小,只需要加一个启动期间的少量 CPU 开销。绝对使用 uint8_t
,而不是 int
。从 8 位内存位置加载 32 位临时文件并不比从零填充的 32b 内存位置慢。 (在 x86 上额外的 movsx
指令,而不是使用内存操作数,将在减少缓存污染方面为自己付出更多的代价。RISC 机器通常不允许内存操作数,所以你总是需要一条指令来加载值存入寄存器。)
此外,sqrt
是 Sandybridge 上的 10-21 周期延迟。如果您不经常需要它,int->double、sqrt、double->int 链并不比 L2 缓存命中差多少。比进入 L3 或主内存更好。如果你需要很多 sqrt
,那么当然,做一个 LUT。吞吐量会好得多,即使您在 table 中跳来跳去并导致 L1 未命中。
你可以通过平方而不是 sqrting 来优化初始化,比如
uint8_t sqrt_lookup[65536];
void init_sqrt (void)
{
int idx = 0;
for (int i=0 ; i < 256 ; i++) {
// TODO: check that there isn't an off-by-one here
int iplus1_sqr = (i+1)*(i+1);
memset(sqrt_lookup+idx, i, iplus1_sqr-idx);
idx = iplus1_sqr;
}
}
您仍然可以获得 sqrt_lookup
为 const
的好处(编译器知道它不能别名)。要么使用 restrict
,要么对编译器撒谎,因此 table 的用户看到一个 const
数组,但您实际上写入了它。
这可能涉及欺骗编译器,在大多数地方声明它 extern const
,而不是在初始化它的文件中。您必须确保这确实有效,并且不会创建引用两个不同符号的代码。如果你只是在初始化它的函数中丢弃 const
,如果编译器将它放在 rodata
中(或者如果未初始化,则只读 bss
内存,如果这在某些平台上可能吗?)
也许我们可以避免对编译器撒谎,方法是:
uint8_t restrict private_sqrt_table[65536]; // not sure about this use of restrict, maybe that will do it?
const uint8_t *const sqrt_lookup = private_sqrt_table;
实际上,这只是一个指向 const
数据的 const
指针,不能保证它所指向的内容不会被其他引用更改。