1

源于鲲鹏,回归社区:GNU Glibc的ARM优化小记

 1 year ago
source link: https://yikun.github.io/2019/12/30/%E6%BA%90%E4%BA%8E%E9%B2%B2%E9%B9%8F%EF%BC%8C%E5%9B%9E%E5%BD%92%E7%A4%BE%E5%8C%BA%EF%BC%9AGNU-Glibc%E7%9A%84ARM%E4%BC%98%E5%8C%96%E5%B0%8F%E8%AE%B0/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

源于鲲鹏,回归社区:GNU Glibc的ARM优化小记

Dec 30 2019

从2019年10月初开始,我们团队开始着手Glibc在aarch64(64)架构下的优化工作,并且在2019年年底,将我们的全部优化贡献给上游开源社区。本文分享我们在Glibc的版本完成的优化以及性能测试结果,同时我们也尝试着将优化的思路进行总结,希望对其他项目的优化提供一些思路。

1.1 什么是Glibc?

我们先看看官方的解释:

The GNU C Library project provides the core libraries for the GNU system and GNU/Linux systems, as well as many other systems that use Linux as the kernel.

Glibc的全名是The GNU C Library,它为GNU系统、GNU/Linux系统以及提供了核心的底层库。比如,我们平常使用的memsetstrlen等等这些非常常用的接口都由这个库提供。

1.2 为什么要优化?

在计算领域的水平场景,例如大数据、数据库、Web等领域都直接或者间接地依赖着Glibc,举个简单的例子,在数据库的代码中,我们经常使用memcpy接口,对变量进行复制,调用频次也异常的高。如果在数据复制的过程中,性能能够有所提升,那么对上层软件的性能提升也是显而易见的。

1.3 做了什么优化?

根据我们的分析,字符、内存和锁操作是最基础也是最重要的基本接口,因此,我们选择了对这三种类型的接口优先进行优化。在实现优化中,我们利用了Glibc的indirect function这一机制,即会根据CPU、CPU arch去自动选择匹配的函数。这一机制让我们的实现,更加灵活,也对现有系统影响最小。

下图为我们这次优化主要接口:

image

在上游社区的推进过程中,我们始终坚持Upstream First的原则,希望能够将鲲鹏优化的收益共享给整个生态,真正做到源于鲲鹏,回归社区

所以,可以看到我们的优化大部分(橙色部分)都贡献到了上游社区的AArch64的generic实现中,从而使得整个生态都能够受益,而小部分(绿色部分)针对于Kunpeng CPU的特殊优化则保持了单独实现。

我们知道在一般的开发中,小字节数据操作的使用频率,是远远的大于大字节数据操作的使用频率,而对于大数据和数据库的场景,则有可能会出现很多大字节数据操作的使用。因此,其实我们的一个优化原则是:在保证中小字节没有负优化的前提下,提升大字节数据操作的性能

本节我们将一一解析在我们贡献的过程中,每个接口优化的关键点,并且尽可能的写的通俗易懂,希望能通过这些干货,给大家在其他的优化中带来启发。

2.1 memcmp,每次做更多,总时间更少

Patch链接:aarch64: Optimized implementation of memcmp

2.1.1 优化思路

对于memcmp的优化,我们的核心思路是通过循环展开让每个周期内做的事情更多,从而减少循环本身的开销。下图可以直观的看出,循环展开带来的性能提升:
image
具体如下:

  1. 扩展循环间隔长度
    memcmp的aarch64原实现是以16bit的长度作为循环的周期长度,在无形中增加了很多次循环的消耗,尤其是在进行大字节数据比较中,有较大的性能损失。因此,我们这次优化的核心思路是:将16bit的循环扩展的64bit的循环,简单的说就是现在一次循环会比较64bit的数据。
  2. 寻址方式优化
    除此之外,我们还改变了LDP的寻址方式,从原来的后变址寻址(Post Index Addressing)变成了偏移寻址(Base Plus index)。

2.1.2 性能测试

image

可以从我们实际的测试结果看到整体在中大字节的性能有不错的提升,尤其是在128字节以上的场景,性能提升更是达到了18%。

2.2 memcpy,他山之石,可以为玉

Patch链接:add default memcpy version for kunpeng920
memcpy优化,因为社区的falkor版本在大、小字节的性能表现,已经很完善,因此最终,我们直接使用了Flakor版本作为优化版本。

Falkor版本的将字符分为3种场景:

  1. 对于small(< 32)的场景,优先处理,避免过多判断,影响性能。
  2. 对于medium(33-128)的场景,做展开,避免多次循环带来的性能损失。
  3. 对于large(>128)的场景,4字节对齐处理,并做循环展开每次循环处理64字节。

有兴趣的可以看看源码的实现链接,整体性能提升13-18%。

2.3 memrchr,站在巨人的肩上

Patch链接:aarch64: Optimized implementation of memrchr

2.3.1 优化思路

memrchr整体的优化思路是,参考memchr设计的魔鬼数字算法,通过汇编实现逻辑适配,实现对特定字符逆向查找的功能,替代原有的C语言实现方案达到优化,具体实现见上链接。

2.3.2 性能测试

image
最终,我们获得了58%的性能提升,最终在大字节的场景,比generic版本提升了4倍左右。

2.4 memset,定向优化,更懂硬件

Patch链接:aarch64: Optimized memset for Kunpeng processor.

2.4.1 优化思路

我们进行了通过循环展开和特殊的定制优化来更好的适配硬件分支预测的特性,从而达到优化的效果。

特别说明的是,对于memset来说,置零场景是非常常用的场景,我们发现原有的实现使用DZ_ZVA指令并未在置零场景有显著效果,反而增加了许多条件分支,因此我们使用set_long代替了置零,由于set_long本身有更少的分支及更少的预测,所以性能与原实现比也有所提升。

2.4.2 性能测试

image

2.5 strcpy,加速的武器,vector loads

Patch链接:aarch64: Optimized implementation of strcpy
strlen使用了neon寄存器,通过vector operations对函数进行了优化,对比原有的汇编实现,在64字节以上的场景,获得了5%-18%的提升:

image

2.6 strlen/strnlen 循环展开,判断更少,性能更优

strlen Patch链接:aarch64: Optimized strlen for strlen_asimd
strnlen Patch链接:aarch64: Optimized implementation of strnlen

strlen和strnlen同样使用了vector operations和循环展开,对主循环仅行了改造

strlen有7%-18%的提升:
image

strnlen有11%-24%的提升:
image

经过上面的介绍,相信大家已经了解了我们是怎么去优化这些函数的版本的,虽说大部分的优化都是比较晦涩的汇编语言,但是其实实际原理还是非常易懂的。

最后,我们再总结下我们应该从哪些方面考虑,去完成优化:

  • 使用Neon汇编指令提高指令速度
  • 使用Prefetch机制充分利用cache
  • 避免非对齐的内存访问
  • 指令重排,减少数据依赖
  • 循环展开,减少高频判断
  • 结合硬件特性,用软件补齐硬件缺陷

4. 写在最后

本书所提及的所有代码,均已贡献到Glibc上游社区,并且随着Glibc 2.31已经在社区完成发布,有需要的可以直接从社区上游获取使用,有任何问题也可以在本文留言。

另外,Glibc优化,也全部合入到集成在当前版本的openeuler中,有兴趣的,也可以直接使用openEuler最新版本进行体验。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK