可移植性是可靠性

2021-05-06 14:31:44

任何熟悉我的开源工作的人都知道我的工作的重点是在可移植性上,但最近对我来说,很多人可能不知道为什么。

Hedley - 一个C / C ++标题,让您利用所有平台(旧标准,C与C ++,不同编译器,旧编译器等)可能无法使用的功能,而不会创建硬依赖性。

便携式代码段 - 织放相关模块的集合,旨在提供对不同功能的相对便携式的访问。例如,内置模块包含编译器特定内置/内部的便携式实现,例如__builtin_ffs和_bitscanforward。

SIMDE - 对于目标不受本地支持它们的目标的SIMD API的实现(例如,编译在霓虹灯上编写的SSE编写的代码),并且还有很多跨编译器的微小差异。

Salieri - Microsoft源注释语言(SAL)的包装器,您可以使用SAL,而不会在Microsoft的编译器中创建硬依赖性。

TinyCthread - 一个图书馆,我维护(虽然最初创建),它实现了C11线程API,它在POSIX和Windows线程API上易于使用ZHARTACITON层。

我已发布用于安装Intel C / C ++编译器的脚本(当您需要处理许可证密钥时,在Oneapi安装琐碎的keial和free之前),NVCC(以前PGI),TI编译器在CI平台上。

我可以继续前进,但希望你明白点:我花了,并继续花费,很多时间和能量制作软件便携式。这是一个并不总是,甚至通常,易于做的事情。

这是一个妙语:我不对便携性深入关心。至少没有编译器的可移植性;我在一组有限的架构上关注宽程度的可移植性(目前大多数x86_64,aArch64,webassembly,电源以及范围RISC-V和S390x)。当然,我编写开源代码,所以人们可以使用它,所以支持,例如,msvc意味着我代码的更广泛的潜在用户群,这是很大的,但这不是我尝试支持msvc的主要原因,而且它肯定不够有理由忍受MSVC的废话。

在为生产目的构建代码时,我真的不会使用克朗和通用通讯社以外的任何东西。我不使用Windows,我讨厌Visual Studio,我发现很难在礼貌公司中表达MSVC的看法(虽然最后添加C11支持大大改进了东西)。支持MSVC对我来说是一个巨大的烦恼,所以为什么要烦恼?

编译器的可移植性是一个终端的手段,而不是本身的结束。我真正关心的是写可靠的软件。而且,由于我大多数在C中编写软件,写入可靠的软件是一个非琐碎的任务。实际上,“非琐碎”是轻描淡写:它在极度困难和不可能之间的某个地方。

幸运的是,工具可以提供帮助。很多。几乎每个人都知道(或者至少应该知道)在开发期间逐起来的编译器警告是一个基本的必要性;如果你没有至少使用-wall,那么你就是错误的。 -Wextra(GCC)和-Weverything(Clang)更好,尽管你最终必须处理相当数量的误报......学会这样做,值得。

克朗的诊断倾向于捕捉到GCC捕获的超级赛,但GCC也捕获了一些铿cl的东西;想想一个圆形的venn图,其中一个圆越大,大多数与较小的圆圈重叠。这两者都是一个好主意。我强烈建议您在CI中执行此操作,以确保每次提交都会通过两个编译器,并启用所需的警告,并添加-Werror将这些警告转换为错误。您还应该添加各种消毒器,以及在GCC上的Clang和-Fanalyzer上的Scan-Build。

修复所有警告克朗和GCC发出的是一个很好的开始,但仍有更多的错误才能被抓住!就像gcc和clang支持不同的诊断,msvc也是如此。 MSVC上的/ W4大致类似于-Wextra在GCC上或 - 在Clang上的所有事情,它可以捕获GCC和Clang的很多问题。如果您可以通过GCC,Clang和MSVC运行代码,您将能够在到达用户之前找到并修复更多错误。

如果这对MSVC打扰的原因是不够的,您应该知道它还包括一个奇妙的静态分析仪,类似于扫描 - 构建或-FANALYZER。 imho很容易成为他们编译器的最佳部分。当然,这可能不是一个特别高的酒吧,但我保证真的很好......在我的经验中,它比克朗和GCC的静态分析仪更好,但不如覆盖物的东西。

可悲的是,移植到MSVC往往比在GCC和Clang之间移植更多的工作。 Hedley可以帮助很多,便携式代码段可以提供帮助,并且有很多小抽象库我没有写它可能是非常有帮助的,但赔率非常好,你将结束一些#ifdefs没有你做了什么。

其他编译器怎么样?好吧,他们都抓住了不同的问题。有很多重叠,大部分时间都有一个流行的编译器也会抓住同样的问题,但并非总是如此。例如,Oracle Developer Studio和(至少一些)TI编译器包括可以检查Misra C违规的代码。

不幸的是,为了利用所有这些伟大的工具,您的代码需要在相关编译器上工作。如果您的代码不编译MSVC,良好运气将从静态分析仪中获取任何东西。同样,如果您无法在CLANG上编译代码,则扫描构建不会工作。

它不仅仅是静态分析。通常只是编译和运行您的代码,其他地方可以揭示可能纠正休眠的问题。例如,在Simde中,几乎每个功能都在一个简单的循环上最坏的情况下掉回。添加两个双精度浮点值的向量可能如此如此(简化为清楚起见):

for(size_t i = 0; i<(r.f64)/ sizeof(r.f64 [0]); i ++){r.f64 [i] = a.f64 [i] + b.f64 [一世];}

for(size_t i = 0; i<(r.f32)/ sizeof(r.f32 [0]); i ++){r.f64 [i] = a.f64 [i] + b.f64 [一世];}

在Simde的(相当广泛的)CI测试中,几个编译器点击了这种情况。他们乐于编译代码,它跑得很好。然后msvc失败。我审查了日志,将我指向代码中的相关位置,我很快修复了这个问题,而不会击中默认分支。它发生在我的设置中的其他编译器中,但有一个很好的机会,有人称之为来自其他地方的函数将最终崩溃或(更差)默默错误的数据。

这绝不是一个独特的例子;我经常编写在本地工作的代码,并且在CI上传递大多数配置,仅适用于其他CI配置来捕获该问题。通常这是我的错误,但我也发现了令人惊出的规律性的编译错误。

就像其他编译器一样可以帮助您捕获不同的错误,其他体系结构可以这样做。

一个很好的例子是识别别名违规行为。我不会在这里解释混叠;如果您尚未熟悉该问题,我记得严格的别名是信息性的。什么是严格的混叠规则,为什么我们关心?在C中严格的混叠规则,例子也看起来很好,至少基于非常快速的撇击。

X86_64非常容易宽容侵权(因为它是极其容忍未对准的访问)。 MSVC甚至更加。这意味着在那里有很多代码,通常无意中依赖于别名,这很容易返回以后咬你。仅仅因为您的代码适用于一个编译器的特定目标,某些编译器标志并不意味着如果更改任何这些事情,它将继续工作。

如果您想摆脱潜在的混叠错误,那么可以在ARMv7上运行代码的好方法。 ARMv7架构相对挑剔地对未对准数据相对挑剔,因此在AARCH64或X86上工作正常的代码通常会因侵权而导致ARMv7崩溃。即使您的代码在练习中永远不会在ARMv7上运行,在您的CI设置中也非常值得。无人机提供ARMV7和AARCH64硬件,如果您的代码是开源,您可以免费使用它,但即使只是交叉编译到ARMV7并在QEMU运行您的测试套件,也可以揭示很多问题。

现在,你可能会告诉自己,你永远不会在armv7上运行你的代码,所以为什么要打扰? x86 / x86_64和aarch64似乎一切似乎都可以,这就是你所感兴趣的,所以为什么要去寻找麻烦?

让我告诉你一个故事。这不是事物盛大方案的主要事件,但这是一个非常好的例子。我认为这是一个相当形成的时刻,在我自己的发展中作为程序员,希望其他人也可以从中学习。

2015年,我正在进行大量的数据压缩(南瓜),我注意到了使用LZ4时的崩溃。在某些测试之后,我意识到仅在GCC 5(而不是早期版本)上发生的崩溃,并且仅在-O3(或 - 循环 - Vectorize和-Fvect-Cost-Model中,其中包括在-O3中,经过)。 LZ4当时非常熟悉,我认为这个问题是GCC中的错误。毕竟,早期版本工作,代码是相同的,而且硬件,操作系统和其他一切都一样。崩溃与崩溃的唯一差异是编译版本。

我向GCC提出了一个错误,分钟后,一些GCC开发人员开始看它(侧面注意:GCC开发人员在我的经验中非常有帮助,响应和专业人士。事实证明,BUG在LZ4中,其中违反了别名,触发了一个错位的访问,这导致了崩溃。

如果需要,您可以阅读错误报告;如果你真的不明白为什么别名违规是一个问题,这是一个非常简单的介绍。这是一个重要的教训,但对我来说,它真的驱动了一个更重要的课程:因为你的代码在目标时在一个版本的编译器中工作时,在定位特定架构时并不意味着它将继续在下一个版本中工作。当您依靠未定义的行为时,所有投注都已关闭。人们喜欢说它可以格式化你的硬盘,显然是那个夸张(虽然技术上是真的),但这是一个实际发生的事情的一个好的,真实的例子。

编译器可以(和do)假设未定义的行为是您的代码不依赖它。换句话说,它可以假设未定义的行为无法访问。在这种情况下,由于X86上的64位整数的对准要求(_alignof(int64_t))是8个字节,因此编译器可以假设您永远不会尝试访问与8字节边界不对齐的数据取消引用指向64位整数的指针。从编译器的角度来看,这意味着发射更快的代码是安全的,这假设指向64位整数的指针是对齐的。

您可能知道,编译器一直在添加新的优化。这是一件好事;如果您不得不完成超越重新编译的任何工作,您的代码往往会变得更快。在这种情况下,新的优化“破坏”代码正在工作。当然,代码已经被打破了,但自从它之前工作以来很难知道。

在这种情况下,我争辩说出最佳结果之一:代码崩溃了。是的,崩溃很好。崩溃你知道有些事情出了问题。这是一个比你的数据默默地损坏的更好的结果,而且没有你知道你的结果不正确。唯一比崩溃更好的是编译时错误。

在更新编译器时捕获了这里的错误,交换架构也可以捕获许多错误。当我在ARMv7上开始测试时,我发现SIMDE中发现了大量的别名侵权行为,当我修复他们时,其他架构似乎莫言多重崩溃,特别是在更高的优化水平。 Simde运行的事实并进行了测试,ARMv7意味着代码在所有架构上更可靠,包括X86_64,AARCH64,POWER,S390x等。

捕获错误的另一个伟大诀窍是一个大型内心架构,如S390x(ARM和PPC也支持大endian,但小endian更常见)。如果使用错误类型操作数据,请在大型内部机器上运行您的测试套件,通常会使问题变得非常明显,因为结果可能是垃圾。如果您无法访问S390x(谁?),QEMU的S390X实现非常出色。

WebAsseMbly除了作为自己的权利中越来越重要的目标之外,往往很擅长捕获界限。即使在addresssanitizer完全沉默时,我还有代码触发D8中的崩溃。

最近有很多焦点,用更安全的语言更换C和C ++,如Go和Rust。我不反对那个; C和C ++通常在今天开始新项目时通常不是正确的选择。也就是说,现在在C / C ++中写了很多代码,它不会很快就会消失。部分解决方案肯定是从C / C ++过渡,但另一部分正在寻找改进C / C ++的方法。你可以称之为“把猪灰嘴上放在猪上”,“把柠檬变成柠檬水”,或只是“必要的邪恶”,但有必要。

Linus Torvalds曾经有着名的话说,“给予足够的眼球,所有的虫子都很浅”。虽然历史并不一定地对此断言善良,但我认为这很清楚,如果我们考虑编译器,静态分析仪,集成测试和其他工具(隐喻)眼球,那么接受本声明的真实性变得更容易;也许不是所有的虫子都很浅,但大部分会变得不那么深。

编写可靠的软件,尤其是语言如C和C ++,是一个非常艰难的问题。您需要所有的帮助,尤其是可以自动化的那种,所以它只是在后台静静地运行,直到它发现问题。这种帮助是比人类更可靠,更便宜,更便宜,更可扩展。

不幸的是,没有一个工具是完美的。最好的选择是深度防守;使用尽可能多的工具,尽可能多地捕获尽可能多的问题。如果你写一个错误,希望你的编译器会抓住它。如果您的编译器错过了它,希望其他编译器(或旧的编译器或更新的编译器)将捕获它。如果其他编译器错过了,希望静态分析仪会抓住它。如果静态分析仪错过了它,希望有一个消毒剂会抓住它。如果Sanitizers错过了,希望其他硬件会抓住它。

可移植性不是答案; 我不认为有一个答案。 然而,可移植性是帮助编写可靠的软件的重要工具,因为他们将其视为最终而不是一种手段。 有时候可移植性是最终目标,但它也是一个更重要的结束的手段:可靠性。 使用尽可能多的工具意味着确保您的代码尽可能多的地方工作。 换句话说,可移植性是可靠性。 编辑:关于Twitter的一些讨论可能对某些人有趣。