面试题答案
一键面试数组下标引用 bigArray[i][j]
与指针访问 *(*(bigArray + i)+j)
的效率分析
-
缓存命中率
- 数组下标引用:在现代编译器中,数组下标引用
bigArray[i][j]
最终也会被转换为指针运算。由于二维数组在内存中是按行存储的(以C/C++语言为例),当按行遍历数组时,如for (int i = 0; i < 10000; ++i) { for (int j = 0; j < 10000; ++j) { /* 处理 bigArray[i][j] */ } }
,内存访问是连续的。这使得缓存可以有效地预取后续的数据,从而提高缓存命中率。 - 指针访问:
*(*(bigArray + i)+j)
本质上和数组下标引用的指针运算相同。在按行遍历的情况下,同样能保证内存的连续访问,缓存命中率与数组下标引用类似。
- 数组下标引用:在现代编译器中,数组下标引用
-
内存连续性
- 数组下标引用:对于二维数组
bigArray[10000][10000]
,在内存中是按行顺序存储的。使用数组下标引用按行遍历,内存访问具有良好的连续性,符合内存访问的局部性原理。 - 指针访问:
*(*(bigArray + i)+j)
按行遍历也能保持内存的连续性,因为它的底层指针运算逻辑与数组下标引用的内存访问顺序一致。
- 数组下标引用:对于二维数组
-
编译器优化
- 数组下标引用:编译器对数组下标引用的优化已经很成熟。编译器可以根据数组的维度信息,进行有效的循环展开、向量化等优化。例如,在循环中处理
bigArray[i][j]
时,编译器可以利用这些信息更好地安排指令执行顺序,提高指令级并行性。 - 指针访问:虽然
*(*(bigArray + i)+j)
本质上也是指针运算,但编译器对于这种复杂指针表达式的优化可能不如简单的数组下标引用直接。在某些情况下,复杂的指针表达式可能会增加编译器优化的难度,导致优化效果不如数组下标引用。
综上所述,在缓存命中率和内存连续性方面,两者表现相似,但在编译器优化方面,数组下标引用
bigArray[i][j]
通常更具优势,效率更高。 - 数组下标引用:编译器对数组下标引用的优化已经很成熟。编译器可以根据数组的维度信息,进行有效的循环展开、向量化等优化。例如,在循环中处理
代码调整提升性能的方法
- 改变数组布局
- 按列存储转换为按行存储:默认情况下,C/C++ 的二维数组是按行存储的。如果应用场景中按列访问更频繁,可以考虑手动实现按列存储。例如,可以使用一维数组模拟二维数组,并手动计算按列访问的地址偏移。但这种方式需要仔细处理地址计算,并且可能会牺牲按行访问的性能优势。
- 分块存储:将大数组分成多个小块,每个小块可以独立进行处理。例如,将
bigArray
分成100x100
的小块,这样在处理小块时,由于数据量小,缓存命中率会更高。可以通过指针运算来管理这些小块的内存访问。
- 指针使用方式
- 指针别名优化:在使用指针访问数组时,要注意避免指针别名问题。例如,如果在函数中传递数组指针,尽量确保指针在函数内部的操作不会导致别名冲突,这样编译器可以进行更有效的优化。
- 指针运算优化:在复杂的指针运算中,可以提前计算一些固定的偏移量,减少运行时的计算开销。例如,如果有一个固定的子数组访问模式,可以提前计算好该子数组的起始指针,然后在循环中直接使用该指针进行偏移,而不是每次都重新计算复杂的指针表达式。