认识Ccache

1. 前言

Ccache是一个C/C++编译器缓存工具,通过缓存编译结果来完成复用以达到加速编译的效果。由于最近在学习这方面,所以本文是作为学习日志来描述其工作方式和一些理解,不会很深入,后面会根据情况可能会聊一些具体的某些方面。

  • 涉及资源

    Ccache4.5.1

  • 这篇文章阅读后可以带来什么?

    阅读完本文将对Ccache的工作方式有一定的理解。

  • 阅读要求

    了解C语言就行。

2. 如何缓存编译结果

2.1 如何标识缓存?

首先我们需要思考一下,在编译代码时,编译结果该如何被标识并可以缓存呢?按照目前的了解,大概其需要以下信息(这里我称其为必备要素,方便后面引用):

  • 源代码
  • 编译参数
  • 编译器环境
  • 依赖的头文件

2.2 哪些结果可以缓存?

理论上来说,几乎任何结果输出都可以被缓存,只要满足上面的条件一致,但是实际情况可能会更为复杂,比如源代码中包含当前时间、计数、路径的宏等,这些变量将影响缓存的复用。

而根据官网说明可以知道,Ccache支持多个平台、编译器和C系列语言,而iOS开发所熟悉的Objective-C语言目前处于部分支持的情况,如对clang module的未完全支持等。

另外,需要说明Ccache只支持单个文件的编译缓存,像多文件编译、链接等不被支持。

3. Ccache的工作方式

现在我们来看Ccache的工作方式,Ccache是通过对诸如上述那些信息使用BLAKE3(一种非常快的哈希算法)来标识缓存,而缓存数据(M文件和R文件,后面会说)自身使用XXH3(一种非常快的非加密算法)进行校验,以检测是否损坏。

3.1 模式的简述

Ccache有两种模式来进行缓存:直接模式和预处理器模式,主要区别如下:

  • 直接模式:对源代码和依赖文件进行哈希以标识缓存
  • 预处理器模式:对源代码运行预处理器(-E)结果进行哈希以标识缓存

当然在哈希时它们也共用一些参数,如编译参数(根据直接模式和预处理模式不同有所取舍)、当前编译器环境等。

这两种模式符合我前面提到的必备要素么?当然符合,其中预处理器结果实际上已经包含了依赖文件内容,下面会具体描述下这两种模式的工作过程。

3.2 直接模式

直接模式下会涉及两个缓存文件:manifest文件和编译结果文件,为了描述方便,我简称它们为M文件和R文件。

  • M文件

    前面我们提到直接模式要对源代码和依赖文件进行哈希处理,具体的过程是:

    1. 对源代码和其他参数(编译参数、环境等)进行哈希,得到M文件的key,将这个key前两位(CocoaPods Spec repo也是这样,一些文件系统会有单目录下文件数量限制)分别作为目录名,剩下的字符串+M结尾作为M文件名;

    2. 依赖文件的信息和R文件的key将被写入到M文件内,其中依赖文件的信息包括文件路径和哈希值,可以通过ccache-dump-manifest命令dump出可读的内容:

      # 已省略大量不相关内容
      File paths (34):
      0: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_off_t.h
      1: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_ctermid.h
      2: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/stdio.h
      3: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_null.h
      4: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ssize_t.h
      5: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_size_t.h
       File infos (6): 
      0:
        Path index: 0
        Hash: 9c5f2p4flfe5olliovo81a6rhlo1g9f3s
        File size: 1460
        Mtime: -1
        Ctime: -1
      1:
        Path index: 1
        Hash: 24f162jilj3eo1anljpnq62j32kurcdhq
        File size: 1128
        Mtime: -1
        Ctime: -1
      2:
        Path index: 2
        Hash: 32060qi7uugchban922p0aa6bp73r21mm
        File size: 2206
        Mtime: -1
        Ctime: -1
      3:
        Path index: 3
        Hash: e7ff0a1l5vp9205224vc0rfgukoecb0me
        File size: 1429
        Mtime: -1
        Ctime: -1
      4:
        Path index: 4
        Hash: 21cav5o5ses0osj5ve5g2tsvi9ldb40rc
        File size: 1473
        Mtime: -1
        Ctime: -1
      5:
        Path index: 5
        Hash: 24a5ccurb228dh6otrn71aifbo3msnlbu
        File size: 1468
        Mtime: -1
        Ctime: -1
       Results (1):
      0:
        File info indexes: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
        Key: 09bfcne1ue9a3flgrrfspoh8ci8jodl76
      复制代码

      当然也可以从Ccache的源码中看到相关结构:

      struct ManifestData
      {
        // Referenced include files.
        std::vector<std::string> files;
      
        // Information about referenced include files.
        std::vector<FileInfo> file_infos;
      
        // Result keys plus references to include file infos.
        std::vector<ResultEntry> results;
        ...
      }
      复制代码

      可以看出M文件的内容分为三组:

      • File paths (files): 依赖文件的路径
      • File infos (file_infos): 依赖文件的内容相关信息,如哈希值和对应上面File paths的index
      • Results(results): R文件的信息,result key和对应的File infos的index
    3. 在M文件中查找R文件的key,此时会倒序遍历results数组,如果对应的依赖文件的哈希都能对上,说明这是正确的result。

      • 在M文件中查找result时, result的 key为什么可以这样被确定呢?

        result key也是一个哈希值,计算方式有两种,会在后面介绍依赖模式时具体解释,但是在直接模式下查找缓存时实际上是还没有进入计算result key的阶段的,那么是如何确定的呢?由于一个M文件对应的源文件和其他参数都是一致的,只有依赖文件内容的变化会导致一个M文件内产生多个result,而在运行时所获取到的依赖文件是最新的,所以此时可以通过匹配index对应的file_info中的哈希值来得到正确的key。

  • R文件

    R文件作为一个缓存容器会包含其对应的所有编译结果,和M文件一样,前面获取到的result的key也是以如此方式来标识R文件的路径,只不过结尾是R,可以通过ccache的-dump-result命令dump出可读的内容:

    Magic: ccac
    Entry format version: 0
    Entry type: 0 (result)
    Compression type: zstd
    Compression level: 0
    Creation time: 1645155482
    Ccache version: 4.5
    Namespace:
    Entry size: 5243
    Result format version: 0
    # 两个编译输出缓存,包含一个.d文件(使用了-MD)
    Embedded file #0: .o (752 bytes)
    Embedded file #1: .d (4427 bytes)
    复制代码

3.3 预处理器模式

预编译器模式较为简单,只涉及一个编译结果文件,通过预编译结果的输出和一些编译参数哈希之后得到一个result key来直接查找缓存。

3.4 依赖模式

依赖模式并不是一个和直接模式/预编译器模式类似的单独缓存查找模式,在关闭依赖模式时,直接模式如果没有命中缓存,会触发进入预编译器模式,使其作为一个备选项进行工作,并在之后将生成的result key写入到前面直接模式的M文件中;而打开依赖模式后,便不会触发预编译器模式,如果直接模式没有命中缓存,就会直接进入实际编译过程,并通过.d文件等相关信息来生成result key写入M文件。

  • 前面提到M文件中会保存依赖文件信息,依赖文件信息是从哪获取的?

    在依赖模式关闭时,由于会触发预编译器模式,此时会由预编译器的输出来得到依赖文件信息;而依赖模式打开后,会在实际编译过程中生成的.d文件中提取依赖文件信息。

  • 为什么需要预编译器模式?

    直接模式由于任何参数或内容发生变化都会引起缓存命中失败,而预编译器模式下某些不影响结果的参数和内容变化依然能够命中缓存;而两种模式同时启用,避免了每次都需要预编译的成本。

3.5 总结

为了使流程更加易懂,我画了个流程图:

Untitled.png

4. 结尾

感谢阅读,文中不免有很多主观想法和一些勘误,或者你认为不对的地方,如果能和我聊聊或者评论留下你的想法,非常欢迎!

5. 参考资料

猜你喜欢

转载自juejin.im/post/7066384052869136397
今日推荐