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

2019-12-20 10:51:24 +08:00
 everlost

像下面这个简单的例子,一个类模板,只有一个指针成员 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()函数都被优化掉了.

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

4806 次点击
所在节点    C++
27 条回复
ai277014717
2019-12-20 10:59:19 +08:00
昨天看了微信的优化包大小的文章,有提到部分模版代码可以由虚函数可以减少模版代码推迟到运行时获取信息,来减少包大小。
zwhfly
2019-12-20 12:13:14 +08:00
优化方面,万事皆可能,只要遵守 as if 规则。
zwhfly
2019-12-20 12:15:47 +08:00
对于主流编译器实现来说,只能说有时候有这个优化,很多时候没有。
zwhfly
2019-12-20 12:20:04 +08:00
而且模板展开前进行这个分析的话,T *大小也不一定固定,比如 using T = void (Class::)(void),咦,我 syntax 没错吧?(成员函数指针)
(这条没仔细分析,可能没这回事。。。)
zwhfly
2019-12-20 12:22:22 +08:00
另外如果外面取这个函数的指针的话,as if 规则要确保两个版本的函数地址不一样,可能会阻碍这类优化。
iceheart
2019-12-20 12:30:13 +08:00
g++开-S,直接看汇编代码
everlost
2019-12-20 14:11:08 +08:00
@zwhfly 不仅没错,而且我完全看不懂,我是刚入门的状态."外面取函数指针,as if 规则要确保这两个版本的函数地址不一样",果真这样的话,那我的设想基本上没戏了.
zwy100e72
2019-12-20 14:58:03 +08:00
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
2019-12-20 15:08:10 +08:00
这个只与编译器的实现有关吧,而且 gcc、clang、微软的 cl 有各自的实现方法……我感觉楼主完全没必要纠结这种茴香豆的细节啊……最多知道未优化版本的大致实现就差不多了
Raymon111111
2019-12-20 15:18:02 +08:00
这个肯定是编译器相关的

但确实是一个优化手段

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

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

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

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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/630765

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX