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

c++的模板在编译期间有"合并优化"的行为吗?还是说必然"一种类型生成一份代码"?

  •  
  •   everlost · 2019-12-20 10:51:24 +08:00 · 5013 次点击
    这是一个创建于 1807 天前的主题,其中的信息可能已经有所发展或是发生改变。

    像下面这个简单的例子,一个类模板,只有一个指针成员 ref,一个打印函数 print().

    template <class T>
    class Refer{
      T* ref;
      Refer(T* _ref){
        this->ref = _ref;
      }
      void print(){
        printf("%p", this->ref);
      }
    }
    
    //测试
    int main(){
      int i;
      char c;
      Refer<int> i_ref(&i);
      Refer<char> c_ref(&c);
      i_ref.print();
      c_ref.print();
    }
    

    虽然 ref 会指向各种 T 类型(上面的例子里是 char 和 int),但鉴于 ref 字段本身的 size 是确定的,而 print()函数也并不访问 T 的内部,编译器似乎生成一份代码就够用了.对吗?或者说,编译时为了类型检查,可以给 char 和 int 各自生成一份代码,但到了链接,也许应该"优化合并"成一份代码?因为这两份代码的汇编似乎是一样的. c++的类型信息不需要带到运行时.

    我用 g++ -O 编译(默认),发现构造函数和 print()函数分别生成两份.(一共 call 了 4 个不同的函数) 用 g++ -O3 编译,构造和 print()函数都被优化掉了.

    我自己还在努力测试中,各位如有类似的经验能否分享一下?先说声感谢.我最终想搞清的是,模板是不是一定会引起代码膨胀,感觉对指针类型的模板类,又不解引用的话,"一个类型生成一份代码",有些浪费.

    27 条回复    2019-12-20 22:44:41 +08:00
    ai277014717
        1
    ai277014717  
       2019-12-20 10:59:19 +08:00
    昨天看了微信的优化包大小的文章,有提到部分模版代码可以由虚函数可以减少模版代码推迟到运行时获取信息,来减少包大小。
    zwhfly
        2
    zwhfly  
       2019-12-20 12:13:14 +08:00 via Android   ❤️ 1
    优化方面,万事皆可能,只要遵守 as if 规则。
    zwhfly
        3
    zwhfly  
       2019-12-20 12:15:47 +08:00 via Android
    对于主流编译器实现来说,只能说有时候有这个优化,很多时候没有。
    zwhfly
        4
    zwhfly  
       2019-12-20 12:20:04 +08:00 via Android
    而且模板展开前进行这个分析的话,T *大小也不一定固定,比如 using T = void (Class::)(void),咦,我 syntax 没错吧?(成员函数指针)
    (这条没仔细分析,可能没这回事。。。)
    zwhfly
        5
    zwhfly  
       2019-12-20 12:22:22 +08:00 via Android
    另外如果外面取这个函数的指针的话,as if 规则要确保两个版本的函数地址不一样,可能会阻碍这类优化。
    iceheart
        6
    iceheart  
       2019-12-20 12:30:13 +08:00 via Android
    g++开-S,直接看汇编代码
    everlost
        7
    everlost  
    OP
       2019-12-20 14:11:08 +08:00
    @zwhfly 不仅没错,而且我完全看不懂,我是刚入门的状态."外面取函数指针,as if 规则要确保这两个版本的函数地址不一样",果真这样的话,那我的设想基本上没戏了.
    zwy100e72
        8
    zwy100e72  
       2019-12-20 14:58:03 +08:00   ❤️ 1
    gcc / g++ 默认优化级别是 -O0 而不是 -O ( -O1 )。

    楼主观察到的默认级别下是关闭所有优化,所以可以看到 4 个不同的函数。-O3 级别下观察到函数调用没有了是函数被内联( inline )了,具体由 -finline-functions 控制,内联的原因是内联开销小于 threshold,这一点可以通过这个链接观察到[1].

    在编译阶段,相同编译单元中相同模板参数的模板会只进行一次实例化;链接期间,还没有被内联的相同模板参数的模板函数实例会只保留一份实现。

    想要进一步了解优化相关内容,建议楼主阅读下 gcc 的优化命令行参数列表[2].

    [1]: https://godbolt.org/z/LxR3W6
    [2]: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
    0x11901
        9
    0x11901  
       2019-12-20 15:08:10 +08:00
    这个只与编译器的实现有关吧,而且 gcc、clang、微软的 cl 有各自的实现方法……我感觉楼主完全没必要纠结这种茴香豆的细节啊……最多知道未优化版本的大致实现就差不多了
    Raymon111111
        10
    Raymon111111  
       2019-12-20 15:18:02 +08:00   ❤️ 1
    这个肯定是编译器相关的

    但确实是一个优化手段

    (不懂 C, 但是 JVM 里有类似的优化
    everlost
        11
    everlost  
    OP
       2019-12-20 15:25:04 +08:00
    @0x11901 不是茴香豆啊,像 shared_ptr 就是模板,在代码尺寸严苛的条件下,一种类型 shared_ptr 消耗一套代码,项目里几十种类型,膨胀几十倍,也是值得考虑的考虑的呀.
    lrxiao
        12
    lrxiao  
       2019-12-20 16:00:39 +08:00
    shared_ptr 你还能合并 operator*和 operator->呢? 内部控制块可能是根据 size 做 aligned_storage 之类的

    编译器应该不会做这种优化, 当然我也没试过 Os, 不过根本用不到这种优化, 因为类型本身就泾渭分明.

    实在不行你就另外写个模板分派上去, 相当于一个 aligned_storage
    augustheart
        13
    augustheart  
       2019-12-20 16:09:58 +08:00
    不一定,需要具体情况具体分析。具体的阈值在哪我不知道。
    augustheart
        14
    augustheart  
       2019-12-20 16:11:33 +08:00
    所以不要把正确性依赖到编译器的优化上。
    0x11901
        15
    0x11901  
       2019-12-20 17:33:07 +08:00
    @everlost emmm……但是话又说回来,就算是编译了几十套 shared_ptr,你也得用啊,难不成你打算手写裸指针自己做引用计数或者 new、delete ?如果条件已经苛刻到这点二进制代码都不愿意增加的话,我感觉还不如选择 C 或者更底层的语言来做……
    zwhfly
        16
    zwhfly  
       2019-12-20 17:59:53 +08:00 via Android
    @everlost 理论上,可以共用一个函数体,然后用 jmp 指令跳转到函数体,每个 jmp 指令的地址都不一样呀,嘿嘿
    hehheh
        17
    hehheh  
       2019-12-20 18:40:01 +08:00 via iPhone
    如果依赖编译器优化,你的代码就不是可靠的。prime 有一章专门讲模版实例化的,因为这个需要你自己去搞。

    说简单点: 编译器的优化不是你应该考虑的问题,因为如果你的代码在不同的编译器下有不同的表现的话。你的代码是不合格的。
    hehheh
        18
    hehheh  
       2019-12-20 18:42:20 +08:00 via iPhone
    你研究的太偏了,而且没必要。с++的话,能把 prime 看两遍就好了。其他的 effective,modern effective l 可以看一遍,有个大概印象就够了
    secondwtq
        19
    secondwtq  
       2019-12-20 19:49:20 +08:00   ❤️ 1
    查了下,这个叫 Identical Code Folding
    不过我现在只能通过 gold 来触发,暂时没搞懂为啥编译器自己不做掉:
    gcc -fuse-ld=gold -ffunction-sections -Wl,--icf=safe ./ipo.cpp
    secondwtq
        20
    secondwtq  
       2019-12-20 19:50:20 +08:00
    哦对了,如果想要避免 Inlining 造成的干扰,可以在函数上加 __attribute__((noinline))
    secondwtq
        21
    secondwtq  
       2019-12-20 20:01:06 +08:00
    http://hubicka.blogspot.com/2015/04/GCC5-IPA-LTO-news.html

    "On the other hand proving that two functions are identical in compiler is much harder than comparing a binary blobs with relocations though. Not only the instructions needs to match each other, but all the additional meta-data maintained by the compiler needs to be matched and merged. This include type based aliasing analysis information, polymorphic call contexts, profile, loop dependencies and more. For this reason the pass does not replace Gold's feature."

    可能是因为这个 ... 大概也能解释为啥 MSVC 也是在 linker 里面做
    qieqie
        22
    qieqie  
       2019-12-20 20:12:18 +08:00 via iPhone
    你举的例子里这种情况写个 union 就行了,不需要 template
    secondwtq
        23
    secondwtq  
       2019-12-20 20:41:11 +08:00
    LLVM 有点硬核啊 ... 只运行这个 Pass: http://llvm.org/docs/MergeFunctions.html 就能实现楼主要的效果 ... 只是默认没打开

    clang++ -O0 -Xclang -fmerge-functions ./ipo.cpp

    原 IR:
    %13 = call i32 @_ZL8bisearchIiEiP5ReferIT_Ei(%class.Refer* %4, i32 10)
    %15 = call i32 @_ZL8bisearchIjEiP5ReferIT_Ei(%class.Refer.0* %5, i32 10)

    优化后的 IR:
    %13 = call i32 @_ZL8bisearchIiEiP5ReferIT_Ei(%class.Refer* %4, i32 10)
    %15 = call i32 bitcast (i32 (%class.Refer*, i32)* @_ZL8bisearchIiEiP5ReferIT_Ei to i32 (%class.Refer.0*, i32)*)(%class.Refer.0* %5, i32 10)

    另外真心佩服这个文档写得比代码还多的 ... 说实话 LLVM 里面文档写得这么详细的 Pass 不多
    GCC 我不熟悉,还是去某东买茴香豆吧 ...
    everlost
        24
    everlost  
    OP
       2019-12-20 20:52:45 +08:00
    @hehheh  冤枉,我绝没有依赖编译器行为的打算.只是有点儿担心类似 shared_ptr 之流的模板造成的代码膨胀.
    everlost
        25
    everlost  
    OP
       2019-12-20 21:56:55 +08:00
    @secondwtq  非常感谢!先看了前半部分,英文烂原谅,回头我再慢慢看...看到有人在做这个东西我就放心了好多.他文档里提到了相似函数的识别算法,我觉得这种算法,由 c++编译器的前部来配合支持更好,前头先把"模板"生成的函数和结构体(毕竟这块儿是重灾区)打上标记,优化时重点检查,不然 N*N 的复杂度,怕会一直被人家拒绝.当然我随便说的...编译器挺有意思的,何时我也能研究一下就好了.哎.
    secondwtq
        26
    secondwtq  
       2019-12-20 22:23:24 +08:00
    @everlost 你仔细看的话会发现那个 n*n 的算法只是面试的时候做题给的第一版 ... 他后面紧接着介绍了两种优化,一种是 O(lgn) 的 treeset 一种是 O(n) 的 hash,但是实际测试 hash 要慢一点,所以就上了个 treeset

    之后又加了一个 hash 来做 candidate 的预处理
    不知道为啥默认没启用
    secondwtq
        27
    secondwtq  
       2019-12-20 22:44:41 +08:00
    找到了这个东西 https://groups.google.com/d/msg/llvm-dev/mJFOYABEyKs/PXcZ7h4OGwAJ
    看起来是担心做得不够安全
    不过 Rust 和 Swift 是默认开启的

    以及这个新鲜出炉的黑科技 paper,号称可以合并任意函数 ... http://homepages.inf.ed.ac.uk/hleather/publications/2019_functionmergesequencealign_cgo2019.pdf
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   918 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 21:20 · PVG 05:20 · LAX 13:20 · JFK 16:20
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.