访问硬编码数组和 运行 时间初始化数组之间有什么性能差异吗?

Is there any performance difference between accessing a hardcode array and a run time initialization array?

例如,我想使用数组SQRT[i]创建一个平方根table来优化一个游戏,但是我不知道在访问SQRT[i] 的值:

  1. 硬编码数组

    int SQRT[]={0,1,1,1,2,2,2,2,2,3,3,.......255,255,255}
    
  2. 在 运行 时间生成值

    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_lookupconst 的好处(编译器知道它不能别名)。要么使用 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 指针,不能保证它所指向的内容不会被其他引用更改。