是否还有理由在C ++代码中使用`int`?

时间:2018-02-11 07:38:36

标签: c++

许多样式指南(例如Google)建议在为数组建立索引时使用int作为默认整数。随着64位平台的兴起,大多数时候int仅为32位,而不是平台的自然宽度。因此,除了简单的说法,我认为没有理由保持这种选择。我们清楚地看到编译以下代码的位置:

double get(const double* p, int k) {
  return p[k];
}

汇编成

movslq %esi, %rsi
vmovsd (%rdi,%rsi,8), %xmm0
ret

其中第一条指令将32位整数提升为64位整数。

如果代码转换为

double get(const double* p, std::ptrdiff_t k) {
  return p[k];
}

现在生成的程序集

vmovsd (%rdi,%rsi,8), %xmm0
ret

清楚地表明,使用std::ptrdiff_t而不是使用int时,CPU感觉更舒适。许多C ++用户已经转移到std::size_t,但我不想使用无符号整数,除非我真的需要模2^n行为。

在大多数情况下,使用int不会损害性能,因为未定义的行为或有符号整数溢出允许编译器在内部将任何int提升为处理索引的循环中的std::ptrdiff_t ,但我们从上面清楚地看到,编译器感觉不到int。此外,在64位平台上使用std::ptrdiff_t会使溢出不太可能发生,因为当我们看到越来越多的人遇到int溢出时,他们必须处理大于{{1}的整数这些日子变得非常普遍。

从我所看到的,唯一能让2^31 - 1分开的事情似乎是int这样的文字是5,但我看不到它在哪里如果我们将int作为默认整数移动,可能会导致任何问题。

我即将std::ptrdiff_t作为我小公司编写的所有代码的事实上的标准整数。这有什么原因可能是一个糟糕的选择吗?

PS:我同意这个名称std::ptrdiff_t很丑陋的事实,这就是为什么我把它变成了std::ptrdiff_t的原因,看起来好一点。

PS:据我所知,很多人会建议我使用il::int_t作为默认整数,我真的想说明我不想使用无符号整数作为我的默认整数。使用std::size_t作为STL中的默认整数是Bjarne Stroustrup和视频 Interactive Panel: Ask Us Anything 中标准委员会在时间42:38和1时确认的错误:02:50

PS:就性能而言,在我所知道的任何64位平台上,std::size_t+-都以相同的方式编译* 1}}和int。所以速度没有区别。如果除以编译时常量,速度是相同的。只有在你对std::ptrdiff_t一无所知时才划分a/b,在64位平台上使用32位整数会给你带来轻微的性能优势。但是这种情况非常罕见,因为我认为不能选择离开b。当我们处理矢量化代码时,这里有一个明显的区别,越小越好,但这是一个不同的故事,没有理由坚持使用std::ptrdiff_t。在这些情况下,我建议使用固定大小的C ++类型。

10 个答案:

答案 0 :(得分:105)

有关C ++核心指南的讨论使用了什么:

https://github.com/isocpp/CppCoreGuidelines/pull/1115

Herb Sutter写道,gsl::index将被添加(将来可能是std::index),其定义为ptrdiff_t

  

hsutter于2017年12月26日发表评论•

     

(感谢许多WG21专家对此的评论和反馈   注)

     

将以下typedef添加到GSL

     

namespace gsl { using index = ptrdiff_t; }

     

并为所有容器索引/下标/尺寸推荐gsl::index

     

<强>原理

     

指南建议对下标/索引使用签名类型。   见ES.100至ES.107。 C ++已经使用有符号整数作为数组   下标。

     

我们希望能够教人们写出新的清洁现代代码&#34;   在高警戒级别,这是简单,自然,无警告,和   不会让我们写下“陷阱”#34;关于简单代码的脚注。

     

如果我们没有像index这样具有竞争力的短字   使用intauto,人们仍会使用intauto并获取他们的   错误。例如,他们会写for(int i=0; i<v.size(); ++i)或   for(auto i=0; i<v.size(); ++i)广泛存在32位大小的错误   使用过的平台和for(auto i=v.size()-1; i>=0; ++i)   不起作用。我认为我们不能教for(ptrdiff_t i = ...   直面,或人们会接受它。

     

如果我们有饱和算术类型,我们可能会使用它。除此以外,   最好的选择是ptrdiff_t,它几​​乎具有a的所有优点   饱和算术无符号类型,只有ptrdiff_t除了   制作普遍的循环风格for(ptrdiff_t i=0; i<v.size(); ++i)   在i<v.size()上发出签名/未签名的不匹配(同样适用于   i!=v.size())今天的STL容器。 (如果未来的STL改变了它   要签名的size_type,即使是最后一个缺点也会消失。)

     

然而,尝试教学将是无望的(并且令人尴尬)   人们经常写for (ptrdiff_t i = ... ; ... ; ...)。 (甚至   该指南目前仅在一个地方使用它,并且这是一个&#34;坏&#34;   与索引无关的示例。)

     

因此我们应该提供gsl::index(稍后可以提出   考虑std::index)作为ptrdiff_t的typedef,我们可以   希望(而不是尴尬)教人们经常写作   (index i = ... ; ... ; ...)

     

为什么不告诉别人写ptrdiff_t因为我们相信它   告诉别人你必须要做的事情会很尴尬   C ++,即使我们做了人们也不会这样做。写ptrdiff_t也是   与autoint相比,丑陋且无法支持。添加点的意义   名称index是为了使其尽可能简单和有吸引力   正确大小的签名类型。

编辑:Herb Sutter的更多理由

  

ptrdiff_t足够大吗?是的。已经需要标准容器   没有比ptrdiff_t可以表示的更多元素,因为   减去两个迭代器必须符合difference_type。

     

ptrdiff_t真的够大,如果我有char的内置数组   或byte大于内存地址空间大小的一半   所以有更多的元素可以用ptrdiff_t表示?是的。   C ++已经使用有符号整数作为数组下标。因此,请使用index   绝大多数用途的默认选项,包括所有用途   内置数组。 (如果你遇到极其罕见的情况   数组或类数组类型,大于地址空间的一半   其元素为sizeof(1),并且您要小心避免   截断问题,继续使用size_t索引   非常特殊的容器。这种野兽在实践中非常罕见,   当它们出现时,通常不会被用户代码直接索引。   例如,它们通常出现在接管的内存管理器中   系统分配和包裹个别较小的分配   其用户使用,或提供自己的MPEG或类似用户   接口;在这两种情况下,只应在内部需要size_t   在内存管理器或MPEG类实现中。)

答案 1 :(得分:36)

我是从一个旧计时器(前C ++)的角度来看待这个...当天被理解为int是平台的本地词,并且可能提供最佳性能。

如果您需要更大的东西,那么您就可以使用它并在性能上付出代价。如果你需要更小的东西(有限的内存,或特定需要固定大小),同样的事情......否则使用int。是的,如果你的值在一个目标平台上的int可以容纳它的范围内,而另一个目标平台上的int不能......那么我们就有了我们编译时特定的特定定义(在它们成为标准化之前我们自己创建)。

但是现在,现在,处理器和编译器要复杂得多,而且这些规则并不容易应用。更难以预测您选择的性能对未知的未来平台或编译器的影响......我们如何才能真正知道uint64_t在任何特定的未来目标上的表现会比uint32_t更好或更差?除非你是处理器/编译器大师,否则你不会......

所以...也许它是老式的,但除非我为Arduino等受限制的环境编写代码,否则我仍然使用int作为我知道将在int内的通用值{1}}我正在编写的应用程序的所有合理目标的大小。并且编译器从那里获取它...这些天通常意味着32位签名。即使假设16位是最小整数大小,它也涵盖了大多数用例..大于这个数字的用例很容易识别并用适当的类型处理。

答案 2 :(得分:17)

大多数程序不会在几个CPU周期的边缘生存和死亡,并且int非常容易编写。但是,如果您对性能敏感,我建议使用<cstdint>中定义的固定宽度整数类型,例如int32_tuint64_t。 它们的优点在于它们在签名或未签名方面的预期行为,以及它们在内存中的大小。此标头还包含快速变体,例如int_fast32_t至少规定的大小,但如果它有助于提高性能,则可能更多。

答案 3 :(得分:14)

没有使用int的正式理由。它不符合标准的任何理智。对于索引,您几乎总是需要签名的指针大小的整数。

那就是说,输入int就像你刚刚对Ritchie说的那样,输入std::ptrdiff_t感觉就像Stroustrup只是踢你的屁股。编码人员也是人,不要给他们的生活带来太多的丑陋。我更喜欢使用 long一些类似index的类型轻松输入,而不是std::ptrdiff_t

答案 4 :(得分:13)

这有点基于意见,但唉,这个问题在某种程度上也有所帮助。

首先,你谈论的是整数和指数,就好像它们是同一个东西,事实并非如此。对于任何类似&#34;整数的排序,不确定大小&#34; ,只需使用int当然,大部分时间,仍然合适。对于大多数应用程序而言,这在大多数情况下都可以正常工作,编译器对此很满意。默认情况下,这很好。

对于数组索引,它是一个不同的故事。

迄今为止还有一个正式的正确的事情,那就是std::size_t。在将来,可能会有std::index_t使得意图在源级别上更清晰,但到目前为止还没有。 std::ptrdiff_t作为索引&#34;工作&#34;但与int一样不正确,因为它允许负指数 是的,这发生在萨特先生认为正确的事情上,但我不同意。是的,在汇编语言指令级别,这支持很好,但我仍然反对。标准说:

  

8.3.4 / 6:E1[E2]*((E1)+(E2))相同[...]由于适用于+的转化规则,如果E1是数组和E2一个整数,然后E1[E2]E2成员E1
  5.7 / 5:[...]如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素[...],则行为未定义

数组订阅是指{em> E2成员E1 。没有像数组的负数元素这样的东西。但更重要的是,带有负加法表达式的指针算法会调用未定义的行为

换句话说:任何大小的签名索引是错误的选择。指数未签名。是的,已签名的索引工作,但它们仍然是错误的。

现在,虽然size_t是正确的选择(一个足够大的无符号整数类型,包含任何对象的大小),但它是否真正可能有争议平均情况的良好选择,或作为默认情况。

老实说,你最后一次创建一个包含10个 19 元素的数组是什么时候?

我个人使用unsigned int作为默认值,因为这允许的40亿个元素对于(几乎)每个应用程序都足够了,并且它已经推动普通用户的计算机非常靠近它limit(如果只是订阅一个整数数组,假定分配了16GB的连续内存)。我个人认为默认为64位索引是荒谬的。

如果您正在编写关系数据库或文件系统,那么是的,您将需要 64位索引。但对于平均而言,正常情况下#34;程序,32位索引就足够了,它们只消耗了一半的存储空间。

当保留大量的索引时,如果我能负担得起(因为数组不超过64k元素),我甚至会去uint16_t。不,我不是在开玩笑。

存储真的是这样的问题吗?贪婪地保存两四个字节是荒谬的,不是吗!好吧,不......

大小可能是指针的问题,所以肯定它也可以用于索引。 x32 ABI不存在无缘无故。你不会注意到不必要的大索引的开销,如果你总共只有少数它们(就像指针一样,无论如何它们都会在寄存器中,没有人会注意到它们的大小是4或8字节)。 / p>

但是请考虑一个槽映射的示例,其中存储每个元素的索引(取决于实现,每个元素两个索引)。哎呀,无论你是每次都打L2,还是每次访问都有一个缓存未命中,它确实会让你感到无比尴尬!更大并不总是更好。

在一天结束时,你必须问自己你付出了什么,以及你得到了什么回报。考虑到这一点,我的风格建议是:

如果它花费你什么,没有什么&#34;因为你只有例如一个指针和一些指数可以保持,然后只使用正式的正确(size_t)。正式是正确的,正确的总是有效的,它是可读的和可理解的,正确的是...... 永远不会错误

然而,如果它花费了你(你可能有几百或者几千或一万个指数),你得到的东西是没有价值的(因为你甚至不能存储2 20 元素,所以无论你订阅2 32 还是2 64 都没有区别,你应该三思而后行太浪费了。

答案 5 :(得分:11)

在大多数现代64位体系结构中,int为4个字节,ptrdiff_t为8个字节。如果您的程序使用了很多整数,那么使用ptrdiff_t代替int可以加倍程序的内存要求。

还要考虑现代CPU经常因内存性能而受到瓶颈。使用8字节整数也意味着你的CPU缓存现在拥有的元素数量是之前的一半,所以现在它必须更频繁地等待缓慢的主存储器(这可能很容易需要几百个周期)。

在许多情况下,执行“32到64位转换”操作的成本与内存性能完全相形见绌。

所以这是一个实际的原因int在64位计算机上仍然很受欢迎。

  • 现在你可能会争论二十几个不同的整数类型,可移植性和标准委员会以及所有内容,但事实是,对于那里写的很多C ++程序,他们正在思考一个“规范”架构,这是经常是他们唯一关心的唯一架构。 (如果您正在为Windows游戏编写3D图形例程,那么您肯定它不会在IBM大型机上运行。)因此,对于他们来说,问题归结为:“我是否需要4字节整数或这是一个8字节的?“

答案 6 :(得分:5)

我给你的建议是不要过多地考虑汇编语言输出,不要过分担心每个变量的确切大小,而不是说&#34;编译器在家里用&#34; 。 (我真的不知道你最后一个是什么意思。)

对于花园式整数,大多数程序都是满的,普通int应该是一个很好用的类型。它应该是机器的自然字大小。它应该是高效使用的,既不浪费不必要的内存,也不会在内存和计算寄存器之间移动时引入大量额外的转换。

现在,有很多更专业的用途,而int不再合适。特别是,对象的大小,元素的数量和数组的索引几乎总是size_t。但这并不意味着所有整数都应该是size_t

签名和无符号类型的混合以及不同大小类型的混合可能会导致问题。但是现代编译器很好地处理了大部分问题以及它们针对不安全组合发出的警告。因此,只要您使用现代编译器并注意其警告,您就不需要选择不自然的类型,只是为了避免类型不匹配问题。

答案 7 :(得分:4)

我不认为使用int真实原因。

如何选择整数类型?

  • 如果是用于位操作,则可以使用无符号类型,否则使用带符号的
  • 如果是与内存相关的东西(索引,容器大小等),你不知道上限,请使用std::ptrdiff_t(唯一的问题是当大小大于PTRDIFF_MAX,这在实践中很少见)
  • 否则请使用intXX_tint(_least)/(_fast)XX_t

这些规则涵盖了int的所有可能用法,它们提供了更好的解决方案:

  • int不适合存储与内存相关的东西,因为它的范围可能比索引小(这不是理论上的东西:对于64位机器,int通常是32 -bit,所以使用int,你只能处理20亿个元素)
  • int不适合存储&#34;一般&#34;整数,因为它的范围可能小于所需的范围(如果范围不够,则会发生未定义的行为),或相反,它的范围可能远大于所需的范围(因此浪费了内存)

唯一可以使用int的原因,如果进行计算,并且知道范围适合[-32767; 32767](标准只保证此范围。请注意,这些实现是免费的提供更大的int,他们通常会这样做。目前int在很多平台上都是32位。

由于上面提到的std类型的编写有点繁琐,可以typedef缩短它们(我使用s8 / u8 /.../ s64 / u64spt / upt(&#34;(un)已签名的指针大小类型&#34;)ptrdiff_t / {{1我已经使用这些typedef 15年了,而且我从来没有写过size_t,因为......)。

答案 8 :(得分:2)

我猜是否更容易打字?但你总是可以typedef

许多API使用int,包括标准库的一部分。这在历史上导致了问题,例如在转换为64位文件大小期间。

由于默认的类型提升规则,比int更窄的类型可以扩展为int或unsigned int,除非你在很多地方添加显式的强制转换,并且在某些实现的某些地方,很多不同的类型可能比int更窄。所以,如果你关心可移植性,这是一个小问题。

N

我也在大多数情况下使用ptrdiff_t作为索引。 (我同意Google的观点,即无符号索引是一个错误的吸引子。)对于其他类型的数学,有int_fast64_tint_fast32_t等等,也会与int一样好或更好。几乎没有真实世界的系统,除了上个世纪的一些已经解散的Unices之外,使用ILP64,但是有很多CPU需要64位数学。从标准来看,技术上允许编译器在int大于32,767时破坏您的程序。

也就是说,任何值得盐的C编译器都会在很多代码上进行测试,这些代码会在内部循环中向指针添加int。所以它不能做任何太愚蠢的事情。当前硬件上的最坏情况是需要额外的指令将32位带符号值签名扩展为64位。但是,如果你真正想要的是最快的指针数学,对于幅度在32 kibi和2 gibi之间的值的最快数学,或者最少浪费的memoey,你应该说出你的意思,而不是让编译器猜测。

答案 9 :(得分:2)

我猜99%的案例没有理由使用int(或其他大小的有符号整数)。但是,仍有一些情况,使用int是一个不错的选择。

A)表现:

intsize_t之间的一个区别是i++可能是int的未定义行为 - 如果iMAX_INT。这实际上可能是一件好事,因为编译器可以使用这种未定义的行为来加快速度。

例如,在这个question中,差异在于利用未定义的行为和使用禁止此漏洞的编译器标志-fwrapv之间的因素2。

如果使用int我的工作循环速度提高了一倍 - 确定我会使用它

B)不易出错的代码

size_t的反转for循环看起来很奇怪,并且是错误的来源(我希望我做对了):

for(size_t i = N-1; i < N; i--){...}

使用

for(int i = N-1; i >= 0; i--){...}

你会得到经验不足的C ++程序员的感激,他们将来必须管理你的代码。

C)使用签名索引进行设计

通过使用int作为索引,您可以用负值表示错误的值/超出范围,这样可以使用更方便的代码。

  1. “如果元素不存在,则查找数组中元素的索引”可以返回-1。要检测此“错误”,您不必知道阵列的大小。

  2. 如果元素在数组中,二进制搜索可以返回正索引,而-index表示元素将插入数组的位置(并且不在数组中)。

    < / LI>

    显然,相同的信息可以用正索引值编码,但代码变得不那么直观。

    显然,还有理由选择int而不是std::ptrdiff_t - 其中一个是内存带宽。有很多内存限制算法,对于它们来说,减少从RAM转移到缓存的内存量非常重要。

    如果您知道,所有数字都小于2^31那么使用int将是一个优势,因为否则只有一半的内存传输只会写入0知道,他们在那里。

    一个例子是压缩的稀疏行(crs)矩阵 - 它们的索引存储为ints而不是long long。因为许多具有稀疏矩阵的运算都是内存绑定的,所以使用32位或64位之间确实存在差异。