华企号 后端开发 既然编译器可以判断一个函数是否适合 inline,那还有必要自己加 inline 关键字吗?

既然编译器可以判断一个函数是否适合 inline,那还有必要自己加 inline 关键字吗?

编译器的 inline 是为了让程序员与编译器互相配合,改善性能的机制,编译器确实可以判断函数是否适合 inline,最简单的例如 getter/setter。然而,inline 是否比不 inline 更好,这中间是有模糊地带的,这就需要一些规则和阈值。对于现代的编译器,我们主动加了 inline,编译器大概只会更改那些 inline 规则中的相应阈值。

然而,实际上在很多时候,我们可以精心地编码,让编译器能更好地进行 inline,让 inline 的效果更好。我这里有一个典型的范例:


的 中 Client-Server 交互,使用了 中的序列化框架,该序列化框架初版完成于 2006 年,后来命名为 febird 库在 google code 上开源,再后来 google code 停止服务,febird 迁移到 github,有段时间重命名为 nark,之后重命名为 ,目前 topling-zip 中代码的 namespace 仍是 terark。从 2006 年至今,除 namespace 名称之外,该序列化框架的接口一直保持稳定,2016 年的时候,针对 C++11 进行了模板推导相关的大幅优化,但仍保持了接口的稳定。以下为原文正文,排版有轻微改动。

原文:
作者:         发表日期: 2009年04月04日
分类:    评论: 条     阅读次数: 2,111 次

优化技术主要有:

(一)优化的 inline

1.1  频繁调用的函数都使用 inline

但是值得注意的是,在 inline 的时候,只 inline 最频繁的分支,很少走到的分支使用非 inline 函数,例如:

void InputBuffer::ensureRead(void* vbuf, size_t length) {
    // 为了效率,这么实现可以让编译器更好地inline这个函数    // inline 后的函数体并尽可能小    if (m_cur+length <= m_end) {
        memcpy(vbuf, m_cur, length);
        m_cur += length;
    } else
        fill_and_ensureRead(vbuf, length);}

一般情况下,如果 length 是个不大的常数值,编译器会把 memcpy 优化成赋值语句。至少在 VC2008 中我观察到了这个优化。

但是这里仍有一种不太优化的情况,在理想的情况下,编译器应该把 m_cur/m_end 都放在寄存器中,只有在寄存器 Spill Out 的时候,才把它们的值从寄存器拷到对象,并调用 fill_and_ensureRead。但实际上编译器没有这么做,每次都存内存读取m_cur/m_end。这可能是编译器观察到 InputBuffer有点大,并且有 。

1.2  MinMemIO/MemIO/AutoGrowMemIO

这个几个效率更高,但只能在内存中操作,编译器的极端优化,在这里得到了体现:在 中,编译器没有做到我想要的优化,但是在这里,编译器做到了,他吧 MinMemIO 放到了寄存器中。

(二)抛弃标准 C++ stream,使用简单、直接的 Stream/Buffer

2.1  可以对各种流进行快速缓冲的StreamBuffer,包括

i.  效率高、最常用的:InputBuffer/OutputBuffer

ii.  效率高、不常用的:SeekableInputBuffer/SeekableOutputBuffer

iii.  效率稍差、不常用的:SeekableBuffer,可读也可写,共享一个位置指针

iv.  这几个Buffer结构简单,操作直接,结合编译器inline可以达到很高的效率,同时可以和实际Stream互操作。

(三)使用 typetraits 识别可以 memcpy 的类,进一步优化

a)  基本类型都可以进行 memcpy,并且这个 memcpy 实际上被优化成了赋值

b)  对稍微复杂的类型,有两种方法:

i.  直接dump,不管它的格式

实现简单,只管dump就行,boost::archive::binary_xxx实现了这种优化,但是它只能对基本类型和用户声明为可直接dump的类优化。并且如果febird也使用这种优化,将不能对Portable格式优化。

ii.  直接dump,再转化格式

就比较复杂,需要一些技巧,febird做到了一点,不管对Native还是Portable格式,都做到了优化。因为序列化使用宏来进行声明,因此,应用代码不用改变,只要认真优化这个宏,就可以做到。febird使用了这样的技巧:

DATA_IO_LOAD_SAVE(MyData1, &a&b&c&d&e&f&g&h)

在这个宏调用中第二个参数 &a&b&c&d&e&f&g&h 被使用了多次,其中有一次展开后将是是这样的:

DataIO_load_vector_impl(dio, *this,
  DataIO_is_realdump<DataIO,0,true>()&a&b&c&d&e&f&g&h, 
  bswap)

其中高亮部分 DataIO_is_realdump<DataIO,0,true>()&a&b&c&d&e&f&g&h 将推导出一个类 DataIO_is_realdump<DataIO, Size, IsDumpable>,其中 Size 是 abcdefgh 的尺寸之和,IsDumpable 是abcdefgh 的 IsDumpable 的 and 结果,DataIO_load_vector_impl 以这个类为参数,进行函数调用的自动分派,如果 Size==sizeof(MyData1) 就说明 MyData 中没有编译器为对齐成员自动产生的 Padding,如果IsDumpable 同时为 true,那么这个类就可以被 dump。但是这里仍然有一个潜在的危险:

如果 &a&b&c&d&e&f&g&h 的顺序和它们在 类定义中出现的顺序不同,并且这个不同是有意为之,而不是粗心大意(虽然这个可能性很低,但不代表不存在),那么这个优化产生的行为将 违背调用者的真实意图。关于这一点,无法进行自动检查,因此使用者需要 特别注意。如果 要测试是否出现了这种错误,可以先禁用这种优化,产生数据,然后使用优化,来读取数据,如果数据格式不同,就说明出了错。

(四)效果

使用了这么多优化, ,平均情况下,如果是基本类型 vector,比 boost 快不了太多,但是对复杂类型,比 boost 要 快20~50倍,如果数据已经过验证,不用担心越界,读取时可以使用NativeDataInput<MinMemIO>,此时速度更加惊人: 比 boost 快 1600 倍!

作者: 华企网通王鹏程序员

我是程序员王鹏,热爱互联网软件开发和设计,专注于大数据、数据分析、数据库、php、java、python、scala、k8s、docker等知识总结。 我的座右铭:"业精于勤荒于嬉,行成于思毁于随"
上一篇
下一篇

发表回复

联系我们

联系我们

028-84868647

在线咨询: QQ交谈

邮箱: tech@68v8.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部