V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tool2d
V2EX  ›  C++

说一个反直觉的事情,一个巨无霸 CPP 源文件的编译速度,要快于实现相同功能的一堆小文件。

  •  
  •   tool2d · 2023-02-14 14:11:37 +08:00 · 5266 次点击
    这是一个创建于 655 天前的主题,其中的信息可能已经有所发展或是发生改变。
    理由是 C++是按照 CPP 文件单独编译的。一堆小 CPP 文件,头文件会很重复包含多次,编译器每次都会解析一遍,就会很慢。

    但是把所有函数硬塞进一个巨无霸 CPP 文件,显然也是很不合理的。

    那么问题来了,有没有办法即兼顾编译速度,又能合理按照小功能划分源文件?
    33 条回复    2023-02-23 20:16:19 +08:00
    learningman
        1
    learningman  
       2023-02-14 14:18:29 +08:00   ❤️ 1
    "头文件会很重复包含多次,编译器每次都会解析一遍,就会很慢"
    #pragma once

    #ifundef AAA
    #def AAA
    #endif
    msg7086
        2
    msg7086  
       2023-02-14 14:19:43 +08:00
    换一个不会每次都会解析一遍的编译器?
    AllenTsui
        3
    AllenTsui  
       2023-02-14 14:20:41 +08:00
    可读性 >>> 运行性能 >>> 编译性能
    yanqiyu
        4
    yanqiyu  
       2023-02-14 14:21:38 +08:00 via Android
    https://en.cppreference.com/w/cpp/language/modules


    @learningman
    OP 指的应该是不同翻译单元都用到的头文件解析耗时,比如 STL 里面模板库的展开,每个翻译单元都要一次

    没啥好方法,要么做一些结构性的更改,要么就用 C++20 的现在编译工具链支持不是很好的 modulws
    codehz
        5
    codehz  
       2023-02-14 14:22:20 +08:00 via iPhone
    不可以并行化吗
    然后如果源文件没变的话,不可以直接用缓存的结果吗
    然后,还有 pch 和 c++模块系统呢
    ysc3839
        6
    ysc3839  
       2023-02-14 14:22:49 +08:00 via Android
    实际开发时基本都会开启预编译头,拆分成多个 cpp 是更快的,而且拆分的话改了其中一个 cpp 后只需要重新编译这个 cpp 即可,不需要全部重新编译,也会快很多。
    cnbatch
        7
    cnbatch  
       2023-02-14 14:23:14 +08:00
    长久以来的做法是,使用 Precompiled Header (预编译头)功能

    如果用上了 C++20 ,可以尝试下 Modules
    tool2d
        8
    tool2d  
    OP
       2023-02-14 14:23:22 +08:00
    @learningman 没说清楚,比如有 10 个小的 cpp ,每个模块都需要重复解析一次头文件。

    也就是一共要解析 10 次。
    acctv2
        9
    acctv2  
       2023-02-14 14:24:21 +08:00 via Android
    没有具体实践过,理论上一大堆小文件链接时间会更长,所以有可能。

    头文件一遍遍解析? ifndef 不行吗
    tool2d
        10
    tool2d  
    OP
       2023-02-14 14:29:00 +08:00
    @ysc3839 感觉预编译头也不是万能的,我看很多开源 linux 项目很少用到预编译头。

    因为这意味着你写 C++代码,不能直接去 include 指定的 fileA.h 和 fileB.h 。必须 include stdafx.h ,其中 stdafx.h 又包含了 fileA.h 和 fileB.h 。

    最后用起来也没那么直观。只能说是 trade off 。
    DIMOJANG
        11
    DIMOJANG  
       2023-02-14 14:33:05 +08:00
    是的,方法可以平衡编译速度和源文件的合理划分。

    一种方法是使用预编译的头文件,它允许你预编译常用的头文件,减少每个包含它们的 CPP 文件的解析时间。
    另一种方法是使用模块,这是 C++中一个较新的功能,允许你编译和链接较小的代码单元。
    此外,你可以将你的源文件组织成逻辑模块或命名空间,以更好地反映你的代码结构,减少不必要的头文件的包含。
    tool2d
        12
    tool2d  
    OP
       2023-02-14 14:55:32 +08:00
    说说我自己的解决办法,就是用.inl (INLINE)文件写代码,IDE 不会编译,只需要在 CPP 里包含.inl 就可以了。

    一个 CPP 是单独的模块,模块里的子功能,全部都在分散在若干个.inl 文件里。

    但是感觉就我一个人这样规划代码结构,有那么一点奇葩。
    codehz
        13
    codehz  
       2023-02-14 14:56:47 +08:00 via iPhone
    @tool2d stdafx 只是一个约定,实践中只要构建系统支持,你可以任意指定某个头文件生成 pch ,并不是一个项目只能有一个 pch
    tool2d
        14
    tool2d  
    OP
       2023-02-14 15:02:33 +08:00
    @codehz 很好奇为什么编译器不能自动无感知来进行 pch 生成。

    让人工指定,还需要去分析每个头文件的复杂程度,对很复杂的头文件才建立 pch ,多累人啊。
    lixile
        15
    lixile  
       2023-02-14 15:11:19 +08:00
    ? 实践中 常用场景不是增量编译次数远大于 全量编译场景吗
    主流的构建系统(指代我熟悉的 linux 体系下的 makefile cmake 等)应该都支持增量编译才对 那明显是多 cpp 更合适吧速度更快 否则单个巨无霸 往往只能使用一个 cpu 核心进行编译 完全没办法并行编译。
    ligiggy
        16
    ligiggy  
       2023-02-14 15:18:27 +08:00
    @tool2d #12 ,你说的就是用纯使用 内联函数,很显然这个方法不通用。把一个又一个的大函数,分解成一个又一个的 内联函数,理想很美好,现实很骨感。

    其次,你的 inl 文件或者说头文件,有这么多的内联实现,不就暴露了大部分接口。

    然后在针对每个类的成员函数,你把一个函数,在多个文件里面实现,是不是也要每个文件定义成类的一部分,编译的时候也很复杂啊,或者你在函数里面将类型当成参数,写着写着就丢失了 C++的灵魂。
    tool2d
        17
    tool2d  
    OP
       2023-02-14 15:28:34 +08:00
    @ligiggy 我用的.inl ,没用到 INLINE 关键字,就只是当成普通的 CPP 源文件使用。

    和普通 CPP 的唯一区别,是.cpp 后缀 IDE 会自动编译成 obj ,而.inl 后缀 IDE 会选择忽视。

    可能和官方有冲突,我也想不出别的扩展名了。
    Nazz
        18
    Nazz  
       2023-02-14 15:33:30 +08:00
    很符合直觉, 只是这样不方便维护.
    Shuenhoy
        19
    Shuenhoy  
       2023-02-14 15:35:40 +08:00
    https://cmake.org/cmake/help/latest/prop_tgt/UNITY_BUILD.html

    各种构建工具有一种被称为 unity build 的功能,可以自动帮你把一些小文件合并。

    不过需要注意,这种情况下比一堆小文件快只是对从 0 编译而言的,如果你需要一边改一边编译,可能还是分多个编译单元更快。

    当然,整这些其实都没啥用,正解还是 C++20 的 module (
    yolee599
        20
    yolee599  
       2023-02-14 15:40:10 +08:00
    并不反直觉。多个文件需要经历预编译,把文件都整合到一起,编译出来的中间文件还要链接到一起组成可执行文件,当然比单独一个文件慢了。
    kidonng
        21
    kidonng  
       2023-02-14 15:40:36 +08:00   ❤️ 2
    一看到标题就想到 sqlite https://opensource.apple.com/source/CPANInternal/CPANInternal-108/DBD-SQLite/sqlite3.c.auto.html

    ** This file is an amalgamation of many separate C source files from SQLite
    ** version 3.6.22. By combining all the individual C code files into this
    ** single large file, the entire code can be compiled as a one translation
    ** unit. This allows many compilers to do optimizations that would not be
    ** possible if the files were compiled separately. Performance improvements
    ** of 5% are more are commonly seen when SQLite is compiled as a single
    ** translation unit.

    虽然说的是运行速度不是编译速度🐶
    tyrantZhao
        22
    tyrantZhao  
       2023-02-14 15:42:06 +08:00
    可读性大于编译速度,都用 cpp 了,就别考虑编译速度了。
    kemchenj
        23
    kemchenj  
       2023-02-14 16:08:02 +08:00   ❤️ 1
    前面的人说了很多了,其实主要就是并发编译多个文件,头文件重复解析导致的,而且还得竞争写入编译缓存,结果就是所有线程都在互等。

    LLVM 大会还有一个 Session 专门讲这件事情
    ,感兴趣的话可以看一下。
    debuggerx
        24
    debuggerx  
       2023-02-14 16:20:41 +08:00
    nullyouraise
        25
    nullyouraise  
       2023-02-14 17:52:31 +08:00 via iPhone
    很多大型项目的构建系统都有这种机制,Unity 、Unreal 等,build 之前会按照一定规则将源文件通过 include 来放入一个更大的 cpp 中一次性编译
    Chromium 曾经也有这种机制叫 Jumbo Build ,但开发团队认为由此获取的编译速度提升与带来的各种编译错误问题相比收益太小,在 2018 年左右时取消了,导致当时我编译耗时翻了四倍
    antonius
        26
    antonius  
       2023-02-14 18:06:00 +08:00
    上面都说了可读性>编译速度,提高编译速度可以试试 ninja + buildcache + icecream
    tool2d
        27
    tool2d  
    OP
       2023-02-14 18:11:29 +08:00
    @antonius 倒不是说写新代码都要一个主文件,而是我最近看的几个古老 CPP 项目代码,很多都是一个文件有 1 万行。

    我就在思考,IDE 有没有很科学的办法,去管理一个巨大代码文件。并让人舒服在里面写代码,和按照功能跳来跳去。
    GordianZ
        28
    GordianZ  
    MOD
       2023-02-14 18:20:24 +08:00
    @tool2d 我二十多万行的 CPP 文件 VS 打开补全啊啥的完全不能用。只要一按 F12 跳转 IntelliSense 就崩溃,全是红波浪。我都是单独开别的编辑器搜索函数名来用……
    icylogic
        29
    icylogic  
       2023-02-14 18:32:48 +08:00   ❤️ 1
    可以参考 cotire ,主要干的事就是
    - precompiled header 预编译头,但是只能解决一部分
    - single compilation unit ( cmake 的 unity build )把 cpp 整合成一个巨大的编译单元

    c 艹 加速编译还有一大堆可以做的事,比如 ccache ,distcc ,pimpl …
    kkocdko
        30
    kkocdko  
       2023-02-15 00:33:37 +08:00 via Android
    是的

    jumbo_build
    litguy
        31
    litguy  
       2023-02-15 08:18:03 +08:00
    @GordianZ 50W 行 C++ 路过,绝大部分是 .hpp 的模板类,编译起来有点慢( x86 还行,国产 ARM 即使 make -j64 也及其慢),但是 vs code 打开还是很顺利,平常都是 ssh remote 方式开发,符号跳转有时候不怎么好用,从来没用过补全功能,单纯打字写代码,感觉还很流畅
    tool2d
        32
    tool2d  
    OP
       2023-02-15 09:31:58 +08:00
    @litguy 没考虑过把 50W 行拆成 10 个小文件,用一个 CPP include 这 10 个小文件吗?

    编辑器表示你们这么玩,我压力很大。

    这种真巨无霸,一部分代码都是程序生成的吧。
    LGA1150
        33
    LGA1150  
       2023-02-23 20:16:19 +08:00
    @kidonng 这个可以用 LTO 解决
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2572 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 03:29 · PVG 11:29 · LAX 19:29 · JFK 22:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.