首页 文章

为什么C编译需要这么长时间?

提问于
浏览
474

与C#和Java相比,编译C文件需要很长时间 . 编译C文件所需的时间比运行正常大小的Python脚本要长得多 . 我目前正在使用VC,但它与任何编译器都是一样的 . 为什么是这样?

我能想到的两个原因是加载头文件和运行预处理器,但这似乎不应该解释为什么它需要这么长时间 .

14 回答

  • 0

    任何编译器的减速都不一定相同 .

    我没有使用Delphi或Kylix但是在MS-DOS时代,Turbo Pascal程序几乎可以立即编译,而等效的Turbo C程序只会抓取 .

    两个主要区别是一个非常强大的模块系统和允许单通道编译的语法 .

    它是C编译器开发人员的优先考虑事项,但C / C语法中也存在一些固有的复杂性,使得处理起来更加困难 . (我不是C的专家,但Walter Bright是,并且在构建了各种商业C / C编译器之后,他创建了D语言.One of his changes是强制执行无上下文语法以使语言更易于解析 . )

    此外,您会注意到通常设置Makefile以便每个文件都在C中单独编译,因此如果10个源文件都使用相同的包含文件,那么包含文件将被处理10次 .

  • 9

    解析和代码生成实际上相当快 . 真正的问题是打开和关闭文件 . 请记住,即使使用包含保护,编译器仍然打开.H文件,并读取每一行(然后忽略它) .

    一位朋友曾经(虽然在工作中感到厌倦),拿走了公司的应用程序并将所有内容 - 所有源文件和头文件 - 放入一个大文件中 . 编译时间从3小时降至7分钟 .

  • 2

    几个原因

    头文件

    每个编译单元都需要数百甚至数千个头文件(1)加载和(2)编译 . 通常必须为每个编译单元重新编译它们中的每一个,因为预处理器确保编译头的结果可能在每个编译单元之间变化 . (宏可以在一个编译单元中定义,它改变 Headers 的内容) .

    这可能是主要原因,因为它需要为每个编译单元编译大量代码,此外,每个头必须被编译多次(每个编译单元包含它一次) .

    链接

    编译完成后,所有目标文件必须链接在一起 . 这基本上是一个单一的过程,不能很好地并行化,并且必须处理整个项目 .

    解析

    语法分析非常复杂,在很大程度上取决于上下文,并且很难消除歧义 . 这需要很多时间 .

    模板

    在C#中, List<T> 是唯一被编译的类型,无论您在程序中有多少个List实例化 . 在C中, vector<int> 是与 vector<float> 完全独立的类型,每个类型都必须单独编译 .

    除此之外,模板构成了编译器必须解释的完整图灵完整的“子语言”,这可能变得非常复杂 . 即使是相对简单的模板元编程代码也可以定义递归模板,这些模板可以创建数十个模板实例 . 模板也可能导致极其复杂的类型,名称冗长,为链接器添加了大量额外的工作 . (它必须比较许多符号名称,如果这些名称可以增长到数千个字符,那可能会变得相当昂贵) .

    当然,它们会加剧头文件的问题,因为模板通常必须在头文件中定义,这意味着必须为每个编译单元解析和编译更多的代码 . 在普通的C代码中,标头通常只包含前向声明,但实际代码很少 . 在C中,几乎所有代码都驻留在头文件中并不罕见 .

    优化

    C允许一些非常戏剧性的优化 . C#或Java不允许完全消除类(它们必须用于反射目的),但即使是简单的C模板元程序也可以轻松生成数十个或数百个类,所有这些类都在优化中内联并再次消除相 .

    此外,编译器必须完全优化C程序 . C#程序可以依赖JIT编译器在加载时执行其他优化,C不会获得任何这样的“第二次机会” . 编译器生成的内容是最优化的 .

    机器

    C被编译为机器代码,这可能比字节码Java或.NET使用更复杂(特别是在x86的情况下) . (这是出于完整性而提到的仅仅是因为它在评论等中提到过 . 在实践中,这一步不太可能只占总数的一小部分 . 编译时间) .

    结论

    大多数这些因素都由C代码共享,实际上编译效率相当高 . 解析步骤在C中要复杂得多,并且可以占用更多的时间,但主要的攻击者可能是模板 . 它们很有用,并使C成为一种更强大的语言,但它们也会在编译速度方面付出代价 .

  • 5

    C被编译成机器代码 . 所以你有预处理器,编译器,优化器,最后是汇编器,所有这些都必须运行 .

    Java和C#编译成字节码/ IL,Java虚拟机/ .NET Framework在执行之前执行(或JIT编译成机器代码) .

    Python是一种解释语言,也编译成字节码 .

    我确信还有其他原因,但一般来说,不必编译为本机机器语言可以节省时间 .

  • 4

    正如已经评论过的那样,编译器花了很多时间实例化并再次实例化模板 . 在这样的范围内,有一些项目专注于该特定项目,并声称在一些非常有利的情况下可观察到30倍的加速 . 见http://www.zapcc.com .

  • 15

    另一个原因是使用C预处理器来定位声明 . 即使有 Headers 保护,.h仍然必须一遍又一遍地进行解析,每次包括它们 . 有些编译器支持预编译的头文件,可以帮助解决这个问题,但并不总是使用它们 .

    另见:C++ Frequently Questioned Answers

  • 37

    我可以想到的两个问题可能会影响C程序的编译速度 .

    POSSIBLE ISSUE #1 - COMPILING THE HEADER: (这可能已经或可能没有通过其他答案或评论解决 . )Microsoft Visual C(A.K.A . VC)支持预编译头文件,我强烈推荐 . 当您创建一个新项目并选择您正在制作的程序类型时,屏幕上会出现一个设置向导窗口 . 如果您点击它底部的“下一步>”按钮,该窗口将带您进入一个包含多个功能列表的页面;确保选中“预编译头”选项旁边的框 . (注意:这是我在C中使用Win32控制台应用程序的经验,但对于C中的所有类型的程序可能不是这种情况 . )

    POSSIBLE ISSUE #2 - THE LOCATION BEING COMPILED TO: 今年夏天,我参加了一个编程课程,我们不得不将所有项目存储在8GB闪存驱动器上,因为我们使用的实验室中的计算机每晚都在午夜擦除,这将抹去我们所有的工作 . 如果为了便携性/安全性等而编译到外部存储设备,可能需要很长时间(即使使用我上面提到的预编译头文件)来编译程序,特别是如果它相当大程序 . 在这种情况下,我给你的建议是在你正在使用的计算机的硬盘上创建和编译程序,无论何时你想要/需要停止处理你的项目,将它们转移到你的外部存储设备,然后单击“安全删除硬件和弹出媒体”图标,该图标应显示为带有白色复选标记的小绿色圆圈后面的小型闪存驱动器,以断开连接 .

    我希望这可以帮助你;如果有,请告诉我! :)

  • 7

    你得到的权衡是程序运行得更快 . 在开发过程中,这对您来说可能是一种冷淡的安慰,但是一旦开发完成,它可能会非常重要,并且程序只是由用户运行 .

  • 740

    在较大的C项目中减少编译时间的一种简单方法是创建一个* .cpp包含文件,其中包含项目中的所有cpp文件并进行编译 . 这会将 Headers 爆炸问题减少一次 . 这样做的好处是编译错误仍然会引用正确的文件 .

    例如,假设您有a.cpp,b.cpp和c.cpp ..创建文件:everything.cpp:

    #include "a.cpp"
    #include "b.cpp"
    #include "c.cpp"
    

    然后通过make everything.cpp编译项目

  • 33

    一些原因是:

    1)C语法比C#或Java更复杂,需要更多时间来解析 .

    2)(更重要的)C编译器生成机器代码并在编译期间进行所有优化 . C#和Java只走了一半,将这些步骤留给了JIT .

  • 16

    编译语言总是需要比解释语言更大的初始开销 . 另外,也许你没有很好地构建你的C代码 . 例如:

    #include "BigClass.h"
    
    class SmallClass
    {
       BigClass m_bigClass;
    }
    

    编译速度比以下慢很多:

    class BigClass;
    
    class SmallClass
    {
       BigClass* m_bigClass;
    }
    
  • 1

    最大的问题是:

    1)无限头重新分析 . 已经提到了 . 缓解(如#pragma一次)通常只适用于每个编译单元,而不是每个构建 .

    2)工具链经常被分成多个二进制文件(极端情况下的make,预处理器,编译器,汇编器,归档器,impdef,链接器和dlltool),所有这些都必须为每次调用重新初始化并重新加载所有状态(编译器,汇编器)或每两个文件(archiver,linker和dlltool) .

    另见这个讨论comp.compilers:http://compilers.iecc.com/comparch/article/03-11-078特别是这个:

    http://compilers.iecc.com/comparch/article/02-07-128

    请注意,comp.compilers的主持人John似乎同意,并且这意味着如果完全集成工具链并实现预编译头文件,那么C也应该可以实现类似的速度 . 许多商业C编译器在某种程度上都这样做 .

    请注意,将所有内容分解为单独的二进制文件的Unix模型是Windows的最坏情况模型(其创建过程缓慢) . 在比较Windows和* nix之间的GCC构建时间时非常明显,特别是如果make / configure系统也只是为了获取信息而调用某些程序 .

  • 4

    大多数答案都有点不清楚,提到C#总是运行较慢,因为执行C中的操作只需要在编译时执行一次,这种性能成本也会受到运行时依赖性的影响(需要加载更多的东西才能运行)运行),更不用说C#程序将始终具有更高的内存占用,所有这些都导致性能与可用硬件的能力更密切相关 . 对于解释或依赖于VM的其他语言也是如此 .

  • 11

    Building C/C++: what really happens and why does it take so long

    软件开发时间的相当大部分不用于编写,运行,调试甚至设计代码,而是等待它完成编译 . 为了使事情变得快速,我们首先要了解编译C / C软件时发生的事情 . 步骤大致如下:

    • 配置

    • 构建工具启动

    • 依赖性检查

    • 编译

    • 链接

    现在,我们将更详细地研究每个步骤,重点关注如何更快地制作它们 .

    Configuration

    这是开始构建的第一步 . 通常意味着运行配置脚本或CMake,Gyp,SCons或其他一些工具 . 对于非常大的基于Autotools的配置脚本,这可能需要一秒到几分钟的时间 .

    这个步骤相对很少发生 . 只需在更改配置或更改构建配置时运行它 . 如果没有改变构建系统,那么要做得更快,就没有太多工作要做 .

    Build tool startup

    当您运行make或单击IDE上的构建图标(通常是make的别名)时,会发生这种情况 . 构建工具二进制文件启动并读取其配置文件以及构建配置,这通常是相同的 .

    根据构建的复杂性和大小,这可能需要几分之一秒到几秒钟 . 这本身就不会那么糟糕 . 不幸的是,大多数基于make的构建系统会导致make为每个构建调用数十到数百次 . 通常这是由递归使用make引起的(这很糟糕) .

    应该注意的是,Make的原因是如此缓慢并不是一个实现错误 . Makefile的语法有一些怪癖,它们实现了一个非常快速的实现,但几乎不可能 . 与下一步结合使用时,这个问题更加明显 .

    Dependency checking

    一旦构建工具读取了其配置,就必须确定哪些文件已更改以及哪些文件需要重新编译 . 配置文件包含描述构建依赖关系的有向非循环图 . 此图通常在配置步骤中构建 . 构建工具启动时间和依赖扫描程序在每个构建上运行 . 它们的组合运行时确定了编辑 - 编译 - 调试周期的下限 . 对于小型项目,这段时间通常是几秒钟左右 . 这是可以忍受的 . Make还有其他选择 . 其中最快的是Ninja,它是由Google工程师为Chromium构建的 . 如果您使用CMake或Gyp构建,只需切换到他们的Ninja后端 . 您不必在构建文件本身中更改任何内容,只需享受速度提升 . 但是,Ninja并没有打包在大多数发行版上,因此您可能必须自己安装它 .

    Compilation

    此时我们终于调用了编译器 . 切割一些角落,这是采取的近似步骤 .

    • 合并包括

    • 解析代码

    • 代码生成/优化

    与流行的看法相反,编译C实际上并不是那么慢 . STL很慢,用于编译C的大多数构建工具都很慢 . 但是,有更快的工具和方法来缓解语言的缓慢部分 .

    使用它们需要一点肘部油脂,但好处是不可否认的 . 更快的构建时间可以带来更快乐的开发人员,更高的灵活性以及最终更好的代码 .

相关问题