首页 文章

是否还有理由在C代码中使用`int`? [重复]

提问于
浏览
179

这个问题在这里已有答案:

许多样式指南(例如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

这清楚地表明,CPU使用 std::ptrdiff_t 比在 int 感觉更有家的感觉 . 许多C用户已经转移到 std::size_t ,但我不想使用无符号整数,除非我真的需要模数 2^n 行为 .

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

从我所看到的,唯一让 int 分开的事情似乎是 5 等文字是 int 这一事实,但如果我们将 std::ptrdiff_t 作为默认整数移动,我看不出它会导致任何问题 .

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

PS:我同意这个名字 std::ptrdiff_t 是丑陋的事实,这就是为什么我把它定义为 il::int_t 这看起来好一点的原因 .

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

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

10 回答

  • 105

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

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

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

    hsutter于2017年12月26日发表评论•(感谢许多WG21专家对本说明的评论和反馈 . )将以下typedef添加到GSL名称空间gsl {using index = ptrdiff_t;并为所有容器索引/下标/大小推荐gsl :: index . 理由指南建议对下标/索引使用签名类型 . 见ES.100至ES.107 . C已经使用有符号整数作为数组下标 . 我们希望能够教会人们在高警告级别编写简单,自然,无警告的“新清洁现代代码”,并且不会让我们写出关于简单代码的“陷阱”脚注 . 如果我们没有像int和auto那样具有竞争力的简短可采用词,人们仍然会使用int和auto来获取它们的bug . 例如,它们将为(int i = 0; i <v.size(); i)或(auto i = 0; i <v.size(); i)写入广泛的32位大小的错误使用平台,并为(auto i = v.size() - 1; i> = 0; i)这是不起作用 . 我不认为我们可以教(ptrdiff_t i = ......有一个正面,或者人们会接受它 . 如果我们有一个饱和的算术类型,我们可能会使用它 . 否则,最好的选择是ptrdiff_t,它有几乎所有饱和算术无符号类型的优点,除了ptrdiff_t仍然使得普遍的循环样式(ptrdiff_t i = 0; i <v.size(); i)在i <v.size上发出有符号/无符号的不匹配( )(对于今天的STL容器,类似于i!= v.size())未来的STL会改变它的size_type进行签名,即使最后一个缺点也会消失 . )然而,尝试教人们常规写作(ptrdiff_t i = ...; ...; ..)是无望的(并且令人尴尬) . ) . (甚至指南目前只在一个地方使用它,这是一个与索引无关的“坏”例子 . )因此我们应该提供gsl :: index(后来可以考虑将其作为std :: index考虑) ptrdiff_t的typedef,所以我们可以(而不是尴尬地)教人们常规写作(索引i = ...; ...; ...) . 为什么不告诉别人写ptrdiff_t?因为我们相信告诉别人你在C中必须做的事情会很尴尬,即使我们这样做,人们也不会这样做 . 与auto和int相比,编写ptrdiff_t太丑陋且无法支持 . 添加名称索引的目的是使使用正确大小的签名类型尽可能简单和有吸引力 .

    编辑:Herb Sutter的更多理由

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

  • 17

    我从一个旧计时器(前C)的角度来看这个... ...当天回想起 int 是该平台的本土词,并且可能提供最佳性能 .

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

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

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

  • 36

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

  • 5

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

    那就是说,输入 int 感觉就像你刚刚对Ritchie说的那样,输入 std::ptrdiff_t 感觉就像Stroustrup刚踢你的屁股 . 编码人员也是人,不要给他们的生活带来太多的丑陋 . 我更喜欢使用long或一些容易打字的typedef,如 index 而不是 std::ptrdiff_t .

  • 2

    这有点基于意见,但唉,唉问题也有点乞求它 .

    首先,你谈论的是整数和指数,就好像它们是同一个东西,事实并非如此 . 对于任何像"integer of sorts, not sure what size"这样的东西,简单地使用 int 当然是大部分时间,仍然合适 . 对于大多数应用程序而言,这在大多数情况下都可以正常工作,并且编译器对此很满意 . 默认情况下,没关系 .

    对于数组索引,这是一个不同的故事 .

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

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

    数组预订是指 E1E2 成员 . 没有像数组的负数元素这样的东西 . 但更重要的是,带有负加法表达式的指针算法会调用未定义的行为 .

    换句话说:任何大小的签名索引都是错误的选择 . 指数未签名 . 是的,签署的指数有效,但它们仍然是错误的 .

    现在,虽然根据定义 size_t 是正确的选择(一个足够大的无符号整数类型,可以包含任何对象的大小),但对于普通情况或者默认情况下它是否是真正的好选择可能是有争议的 .

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

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

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

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

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

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

    但是想想一个槽映射的例子,你可以在其中存储每个元素的索引(取决于实现,每个元素有两个索引) . 哎呀,无论你是每次都打L2,还是每次访问都有一个缓存未命中,它肯定会让你感到有点不同!更大并不总是更好 .

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

    如果它花费你"nothing",因为你只有...一个指针和一些指数要保持,然后只使用's formally correct (that' d size_t ) . 正式的正确是好的,正确的总是有效的,它是可读的和可理解的,正确的是......永远不会错 .

    但是,如果它确实花了你(你可能有几百或者几千或一万个指数),你得到的东西是没有 Value 的(因为你甚至不能存储220个元素,所以无论你是否订阅232或264都不会差异),你应该三思而后行太浪费 .

  • 13

    在大多数现代64位体系结构中, int 是4个字节, ptrdiff_t 是8个字节 . 如果你的程序使用了很多整数,使用 ptrdiff_t 而不是 int 可能会使程序的内存需求翻倍 .

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

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

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

    • 现在你可能会争论二十几个不同的整数类型和可移植性以及标准委员会和一切,但事实是,对于那里写的很多C程序,有's a 1032436 architecture they'重新思考,这往往是他们重新编写3D图形例程的唯一架构Windows游戏,你在IBM大型机上运行 . )对于他们来说,问题归结为:"Do I need a 4-byte integer or an 8-byte one here?"
  • 2

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

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

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

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

  • 11

    我没有't think that there' s real 使用 int 的原因 .

    如何选择整数类型?

    • 如果是用于位操作,则可以使用无符号类型,否则使用带符号类型

    • 如果是与内存相关的东西(索引,容器大小等),你不知道上限,请使用 std::ptrdiff_t (唯一的问题是当大小大于 PTRDIFF_MAX 时,这在实践中很少见)

    • 否则使用 intXX_tint(_least)/(_fast)XX_t .

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

    • int 不适合存储与内存相关的东西,因为它的范围可能比索引小(这不是理论上的东西:对于64位机器, int 通常是32位,所以使用 int ,你只能处理20亿元素)

    • int 不适合存储"general"整数,因为它的范围可能小于所需的范围(如果范围不够则会发生未定义的行为),或相反,它的范围可能比需要的大得多(因此内存浪费)

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

    由于上面提到的 std 类型有点单调乏味,所以可以将它们缩短(我使用 s8 / u8 /.../ s64 / u64spt / upt ("(un)signed pointer sized type")作为 ptrdiff_t / size_t . 我've been using these typedefs for 15 years, and I' ve从来没有写过 int ,因为...) .

  • 4

    我想,更容易打字?但你总能 typedef .

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

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

    Con

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

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

  • 14

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


    一场表演:

    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 作为索引,您可以用负值表示错误值/超出范围,这样可以派上用场并且可以使代码更清晰 .

    如果元素不存在,

    • "find index of an element in array"可能返回 -1 . 要检测此"error",您不必知道数组的大小 .

    如果element在数组中,

    • 二进制搜索可以返回正索引,而 -index 用于元素将插入数组的位置(并且不在数组中) .

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


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

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

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

相关问题