在阅读this question时,我看到了第一个评论:
size_t
的长度不是一个好主意,出于优化/ UB的原因,正确的类型是带符号的类型。
之后是另一条支持推理的评论。是真的吗
这个问题很重要,因为如果我要写例如在矩阵库中,图像尺寸可以为size_t
,只是为了避免检查它们是否为负数。但是,所有循环自然会使用size_t
。这会对优化产生影响吗?
答案 0 :(得分:6)
size_t
被取消签名通常是一个历史性的意外-如果您的世界是16位,则将最大对象大小从32767增大到65535是一个大胜利;在当今的主流计算(标准为64位和32位)中,size_t
是无符号的这一事实在很大程度上是很麻烦的。
尽管无符号类型具有 less 不确定的行为(因为保证了环绕),但实际上它们大多具有“位域”语义,这通常是导致错误和其他不良后果的原因。特别是:
无符号值之间的差异也是无符号的,具有通常的环绕语义,因此,如果您期望负值,则必须事先强制转换;
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 < b\n"; // 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--) {
// ...
}
此外,无符号值不能假定负值的事实主要是稻草人。 您可以避免检查负值,但是由于隐式的有符号-无符号转换,它不会停止任何错误-您只是在怪罪。如果用户使用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);
对于优化部分:在这方面,有符号类型的优点被高估了;是的,编译器可以假设永远不会发生溢出,因此在某些情况下它可以变得更聪明,但是通常这不会改变游戏规则(因为在当今的体系结构中,一般的环绕语义“免费”提供);最重要的是,像往常一样,如果您的探查器发现某个特定区域是瓶颈,则可以对其进行修改以使其运行更快(如果发现有利的话,可以在本地切换类型以使编译器生成更好的代码)。
长话短说:我会选择签名而不是出于性能方面的考虑,而是因为在大多数常见情况下语义通常都不那么令人惊讶/敌意。
答案 1 :(得分:2)
该评论完全是错误的。在任何合理的体系结构上使用本机指针大小的操作数时,有符号偏移量和无符号偏移量在计算机级别上没有区别,因此它们没有空间具有不同的性能属性。
您已经注意到,size_t
的使用具有一些不错的特性,例如不必考虑某个值可能为负的可能性(尽管考虑它可能很简单,就像在接口合同中禁止该值一样简单) )。它还确保您可以使用标准类型的大小/计数来处理呼叫者请求的任何大小,而无需截断或边界检查。另一方面,当偏移量可能需要为负数时,它会排除使用相同类型的索引偏移量,并且在某些方面使其难以执行某些类型的比较(您必须将它们进行代数编写,以使任何一方都不是否定的),但是在使用带符号类型时也会出现相同的问题,因为必须进行代数重排以确保没有子表达式可以溢出。
最终,您一开始应该始终使用对您而言在语义上有意义的类型,而不是尝试为性能属性选择一种类型。只有在出现严重的,可衡量的性能问题,并且看起来可以通过涉及类型选择的折衷方案来改善此问题时,才考虑更改它们。
答案 2 :(得分:1)
我坚持我的评论。
有一种简单的检查方法:检查编译器生成的内容。
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;
}
}
好的,虽然不像clang那样令人印象深刻,但是它仍然生成不同的代码。