对长度使用"size_t"会影响编译器优化
Using `size_t` for lengths impacts on compiler optimizations?
在阅读这个问题时,我看到了第一条评论:
size_t
表示长度不是一个好主意,出于优化/UB的原因,正确的类型是带符号的。
后面跟着另一条支持推理的注释。这是真的吗?
这个问题很重要,因为如果我要写一个矩阵库,图像维度可以是size_t
,只是为了避免检查它们是否为负数。但是,所有循环都将自然地使用size_t
。这会影响优化吗?
size_t
未签名大多是历史上的意外——如果你的世界是16位的,那么从32767到65535的最大对象大小是一个巨大的胜利;在当今的主流计算中(64位和32位是标准),size_t
是无符号的这一事实在很大程度上是一个麻烦。
尽管无符号类型的未定义行为较少(因为包装是有保证的),但它们大多具有"位字段"语义这一事实往往会导致错误和其他不好的意外;特别是:
-
无符号值之间的差异也是无符号的,具有通常的环绕语义,因此如果您可能期望负值,则必须事先强制转换;
unsigned a = 10, b = 20; // prints UINT_MAX-10, i.e. 4294967286 if unsigned is 32 bit std::cout << a-b << "n";
-
更一般地说,在有符号/无符号比较和数学运算中,无符号获胜(因此有符号值被隐式转换为无符号),这再次导致意外;
unsigned a = 10; int b = -2; if(a < b) std::cout<<"a < bn"; // prints "a < b"
-
在常见的情况下(例如向后迭代),无符号语义通常是有问题的,因为您希望边界条件的索引为负数
// This works fine if T is signed, loops forever if T is unsigned for(T idx = c.size() - 1; idx >= 0; idx--) { // ... }
此外,无符号值不能假定负值的事实主要是strawman您可能会避免检查负值,但由于隐式有符号无符号转换,它不会停止任何错误-您只是在推卸责任。如果用户使用size_t
将负值传递给库函数,那么它将变成一个非常大的数字,即使不是更糟,也是错误的。
int sum_arr(int *arr, unsigned len) {
int ret = 0;
for(unsigned i = 0; i < len; ++i) {
ret += arr[i];
}
return ret;
}
// compiles successfully and overflows the array; it len was signed,
// it would just return 0
sum_arr(some_array, -10);
对于优化部分:有符号类型在这方面的优势被高估了;是的,编译器可以假设溢出永远不会发生,所以在某些情况下它可能会非常聪明,但通常情况下这不会改变游戏规则(因为在当前的架构中,一般来说,包装语义是"免费的");最重要的是,像往常一样,如果您的探查器发现某个特定区域是一个瓶颈,您可以只对其进行修改,使其运行得更快(如果您觉得有利,可以在本地切换类型,使编译器生成更好的代码)。
长话短说:我选择签名,不是出于性能原因,而是因为在大多数常见场景中,语义通常不会那么令人惊讶/充满敌意。
该注释完全错误。当在任何合理的体系结构上使用本机指针大小的操作数时,在机器级别上有符号和无符号偏移量之间没有差异,因此没有空间让它们具有不同的性能属性。
正如您所指出的,size_t
的使用有一些不错的特性,比如不必考虑值可能为负数的可能性(尽管考虑它可能很简单,就像在接口契约中禁止它一样)。它还确保您可以使用大小/计数的标准类型处理调用方请求的任何大小,而无需截断或边界检查。另一方面,当偏移量可能需要为负时,它排除了对索引偏移量使用相同类型,并且在某些方面使执行某些类型的比较变得困难(您必须将它们以代数方式排列,以便双方都不是负的),但当使用有符号类型时也会出现相同的问题,因为您必须进行代数重排,以确保没有子表达式可以溢出。
最终,最初您应该始终使用语义上有意义的类型,而不是尝试为性能属性选择类型。只有当有一个严重的衡量性能问题,看起来可能会通过选择类型的权衡来改善时,你才应该考虑改变它们。
我支持我的评论。
有一种简单的方法可以检查:检查编译器生成的内容。
void test1(double* data, size_t size)
{
for(size_t i = 0; i < size; i += 4)
{
data[i] = 0;
data[i+1] = 1;
data[i+2] = 2;
data[i+3] = 3;
}
}
void test2(double* data, int size)
{
for(int i = 0; i < size; i += 4)
{
data[i] = 0;
data[i+1] = 1;
data[i+2] = 2;
data[i+3] = 3;
}
}
那么编译器会生成什么呢?我希望循环展开,SIMD。。。就这么简单:
让我们检查一下螺栓。
有符号的版本有展开SIMD,而不是无符号的版本。
我不打算展示任何基准测试,因为在这个例子中,瓶颈将是内存访问,而不是CPU计算。但你明白了。
第二个例子,只保留第一个赋值:
void test1(double* data, size_t size)
{
for(size_t i = 0; i < size; i += 4)
{
data[i] = 0;
}
}
void test2(double* data, int size)
{
for(int i = 0; i < size; i += 4)
{
data[i] = 0;
}
}
正如你想要的gcc
好吧,没有clang那么令人印象深刻,但它仍然会生成不同的代码。
- 删除一个线程上有数百万个字符串的大型哈希映射会影响另一个线程的性能
- 为什么擦除方法会影响结束方法
- 内联如何影响模块接口中的成员函数
- 为什么返回类型的'const'限定符对标有 __forceinline/内联的函数没有影响?
- 在容量内调整矢量大小时的性能影响
- 重载运算符的范围是什么?它是否会影响作为类成员的集合的插入函数?
- 未达到的情况会影响开关外壳性能
- 循环仅对第一行正常工作.其他行不受 for 的影响
- 处理影响跨不同线程共享对象的定时回调的最佳方法是什么?
- 模板如何影响C++中隐式声明的规则?
- 命名空间信息会影响C++的可读性
- [[可能]]和[[不太可能]]影响程序汇编的简单示例?
- 如何保护非托管应用程序中的字符串不受进程转储的影响
- 检查nullptr是否100%保护内存布局不受segfault影响
- 为什么 std::set.erase(first, last) 会影响从中获取 (first, last) 的容器?
- 发布代码的 gdb 堆栈跟踪可读性如何影响 x64?
- QSql查询行受影响的结果
- 帧缓冲纹理变为白色(片段着色器不会影响它)
- 是否有任何区域设置会影响宽字符编码?
- 使用"静态"如何影响我的代码速度?