x86高性能编程笺注之性能优化

SDN in China

2017年9月13日

技术干货

读者须知:《强者恒强:x86高性能编程笺注》该系列文章将分享x86高性能开发方面的实践和思考。主要内容目录如下。

第一部分:介绍

  • 寻找软件中的Hotspot
  • x86 CPU架构
  • Good/Bad examples

第二部分:性能因素

  • 锁/阻塞
  • CPU核绑定
  • 无锁操作
  • 流水线
  • 分支预测
  • Branch-less编程
  • 实例
  • 数据依赖
  • 循环展开
  • pointer aliasing
  • 实例
  • 缓存
  • 数据对齐
  • Prefetch
  • NUMA
  • 大页
  • 实例
  • 内存
  • 循环
  • 分支
  • 多线程

附:

  • 性能测试工具

前言

高性能软件不仅仅用来构筑市场壁垒。写作高性能软件,是一项杂糅了编程技巧、硬件架构、操作系统、编译器原理等知识与经验的智力享受。这些条件相互促进,又相互制约,像是按照平仄合辙的词牌填词,也像遵循对位赋格来谱曲。性能的提升,会给工程师以巨大的心理奖赏。软件,作为程序员心理活动的副产品,也将一同进入正反馈的循环。所谓“强者恒强”,就是这个道理。

作为时下“软件定义一切”大潮的受益者,基于x86和Linux服务器的软件开发正受到越来越多的关注。相对于它所要取代的传统硬件,x86软件的性能始终是其瓶颈所在。但在整个行业上上下下的努力之下,通用x86处理器也已经在某些应用中达到了专用逻辑电路的性能指标。在这一系列文章之中,笔者想将自己在性能优化方面的实践和思考分享给大家。因为涉及相对底层的操作,所以在文章中选用了方便的C语言作为示例语言。不过主旨仍是讲述原理和实践经验,读者大可不必拘泥于形式。

作为系列的第一篇文章,先和大家探讨一下性能优化的原则。

什么是“性能”

在实践中,我们有时会不自觉地把一些似是而非的指标用来评判一个软件的性能。最常见的,是用一段代码编译后的汇编指令数目来评判软件的性能。越少的指令,就不言自明地代表了越高的性能。也许我们确实可以观察到这一“表象”,但事实上,指令数目少,是软件性能高的既不充分也不必要条件。下面举一个不是看腻了的横纵遍历数组的例子。

有一组固定长度的伪随机数数组,数值范围在0至255之间:

uint32_t i = 0;for (; i < ARRAY_LEN; i++) {

array[i] = rand() % 256;}

我们希望将其中小于128的数字累加求和。除了简单地遍历筛选一遍之外,我们也可以先对数组进行排序之后再执行遍历操作。但“排序”在这里似乎是多余的操作,因为排不排序,都不影响结果的正确性,同时还引入了额外的开销。依据一般“直觉”,排序之后再操作并不是明智的方式。我们的这两种方案表示如下:

#ifdef PRE_SORT

qsort(&array, ARRAY_LEN, sizeof(uint32_t), qsort_cmp);

printf(“Sorted array\n”)

#endif

uint64_t sum = 0;

uint32_t ite_count = 0;

for (; ite_count < 10000; ite_count++) {

for (i = 0; i < ARRAY_LEN; i++) {

if (array[i] < 128) {

sum += array[i];

}

}

}

由宏PRE_SORT控制是否启动排序操作。在操作开始和结束的时候分别记录CPU cycle计数,以便比较执行时间。使用gcc默认参数编译并执行,首先是引入了排序的代码:

# gcc -DPRE_SORT branch.c

# ./a.out

Sum: 20787670000, CPU cycles: 4869610134

下面是未引入排序的执行情况:

# gcc branch.c

# ./a.out

sum: 20787670000, CPU cycles: 14611881138

由于伪随机的原因,两种方式计算的结果都是相同的。而与预想情况不同的是,增加了“多余”排序的程序,执行效率是不排序的3倍(在实验的特定平台上)。当我们调整相关参数,比如数组的长度和迭代的次数,还可以使相差更大。

可以使用rdtsc指令读取CPU的cycle计数,由硬件计数器直接提供,根据CPU主频可以计算出绝对时间。在某些型号的多核CPU上rdtsc指令可能会出现计数误差。但时代一直在进步,Intel已经在新型号的CPU上改良了这一情况。后续的文章中会有详细说明

那么原因是什么呢?要回答这个问题,就必须借助性能测试工具的力量。

在性能优化的过程中,测试工具占据了绝对重要的地位。它给出的结果,几乎可以当作一切性能优化行为的入手点和逻辑起点。在Linux环境下,有数种性能测试和性能监控工具。在后续的文章中,会基于最常用的几种,穿插介绍它们的特性和使用方法。

为防止指令乱序执行导致计时不准,可采用asm volatile(“” ::: “memory”);的方式添加内存屏障。相关概念会在后续内容中逐一介绍

下面使用perf这一工具分析一下两者性能差异的原因,使用命令perf stat ./a.out针对未引入排序的程序,我们得到如下信息:

5,902,900,466 instructions # 0.42 insns per cycle

1,312,563,125 branches # 359.846 M/sec

326,150,404 branch-misses # 24.85% of all branches

第一行是总指令数目,以及IPC(instruction per cycle)指标。第二行是程序执行过程中的分支数量,第三行是分支判断失败的占比。

而在引入排序的程序中,可以看到:

5,939,795,977 instructions # 1.26 insns per cycle

1,321,069,122 branches # 1084.608 M/sec

406,006 branch-misses # 0.03% of all branches

虽然在指令和分支总数方面都多于前者,但因为显著降低的分支判断失败比例,排序的程序还是以3倍的执行速度完成了相同的任务。如果利用工具进一步观察,可以发现未引入排序的代码,在判断一个数字是否大于128的时候产生了太多分支判断失败,因为它所处理的数字大小是随机的,CPU很难做出正确的预测。而在引入排序之后,预测下一个数字是否大于128,就轻松容易得多。

如果更改了gcc的编译参数,比如打开优化选项-O3,会有什么情况发生?另外针对程序的操作,还能想到哪些优化的方法?请动手试一下。:D

分支预测为何关键,它对程序的性能如何施加影响,以及如何据此提高程序的性能等等,都会在后续的章节中详细说明。在这里,我们希望能先明确一个概念,就是什么是性能。通过上面的例子,可以发现我们经常会使用一些指标来帮助我们评判软件的性能,比如指令数,比如Cache命中的次数,比如CPU和内存带宽占用率,甚至可以用服务器耗电功率来间接评价。但这些都可以作为“性能的表征”,却都不是性能本身。“性能”其实是一个很宽泛的概念,在不同的语境下都可以有不同的含义,但如果一定要拉出一根墨绳,以我的理解,性能就是一个纯粹的时间概念。可以在更短的时间内完成一项特定的工作,就是性能高;也只有时间短才是性能高的充要条件。

当然,指令少可能时间短,Cache命中多可能时间短,但是否时间真的短了,也只能是时间说了算。我们平时所提到的xps,即x per second 就隐式表达了这一概念。x 可以是数据包,可以是比特,可以是任何程序所关心的东西在时间尺度per second上的衡量。也许有人会提到,吞吐是不是性能,并发数是不是性能,资源占用量是不是性能?没有问题,这些都可以作为一种“市场方面的性能”加入到产品介绍手册里。但从CPU的角度,它关心什么是吞吐吗?它只是想拼命在最短的时间里执行完指令缓存(CPU Cache)中的指令而已。

基准测试的几点要求

和很多控制系统的架构类似,性能优化的过程是一个负反馈的闭环。上面介绍的例子,其实就可以看作是一次性能优化的过程。当我们不满意程序的性能时候,使用测试工具发现性能的瓶颈。针对瓶颈进行优化,再次测试软件性能,重复这一过程,直至满意为止。在这一闭环中的关键,就是设计有效的基准测试。 一般来说,基准测试需要遵循以下几个原则:

可复现

重复多次可以给出非常近似的测试结果。这意味着需要考虑尽量减少系统等外界因素的干扰,比如同时运行的某个资源占用大户,或者阻塞式IO,以及各种系统的限制,例如NUMA和CPU核数等。基准测试的目的并非是求出一个“平均”或者“最大最小”的性能指标,而是为性能测试工具提供一个良好的观察对象,以便发现和诊断性能瓶颈所在。

覆盖典型执行路径

无论什么类型的测试,其实都是对被测对象输入输出的管理。有些测试可以将被测对象看作黑盒子,但性能基准测试,必须要构建能够覆盖典型执行路径的输入。因为作为在使用环境中大量调用的代码,它们的性能是我们主要关心的内容。同时也是给测试工具发挥最大效用提供必要的条件。而像在功能测试中对corner case的覆盖倒不必过度考虑。

简单易用

为小的功能点设计有针对性的测试例,除了可以使热点更突出,瓶颈定位更准确之外,更主要的是出于程序员自我心理管理的需要。程序员除了完成代码需求之外,也要时刻考虑如何自我心理调控,这比码代码要重要的多。简单易用的测试例跑起来,每次都是“完成了一次迭代”所附带的心理奖赏,显著降低焦虑水平。即便这次不成功,也愿意尝试第二次。而每次都要花大量时间精力才能执行一次的性能测试,会在得到不理想的测试结果之前过早地达到厌烦阈值。同时复杂意味着不稳定,既是测试结果的不稳定,也是心理的不稳定:D而这二者其实是一个东西。同单元测试一样,成熟的测试架构可以帮助我们解决这一问题。

关于性能优化的几点误区

性能优化是一项非常注重实践的活动,但很多时候却容易陷入“清谈”和“玄想”的误区中去。对于性能优化,其实和其他学科一样,还是以实验和观察的结果为根基。“你无法管理你不能测量的东西”,像量子力学一样可以通过理论预言粒子的性质也是可能的,但Intel还没生产量子CPU是不是?

先实现功能,再考虑优化

这是一句经常听到的话,颇有“以大局为重”的意味。性能优化其实是一件和正确性测试很相似的事情,你不能等整个产品都开发完成了才开始跑第一个测试例,这样当(必然)发现了BUG之后,定位和改动都要花费巨大的精力与资源。性能优化同理,尤其是希望以性能为卖点的产品。你在敲下第一行代码的时候,就要开始考虑性能。小到结构体怎么构建,里面的变量如何对齐;大到内存的划分,并行架构的设计。就像最基本的函数单元测试例,小型的性能基准实验跑起来,耗费不了太多时间,却能带来饱满的收益。性能优化确实可以集中一段时间来做,但不能到那个时候才开始考虑如何优化性能。

相信直觉

性能优化是反直觉的。好比我们在本文开头举的例子,多余的操作反而提高了性能。在实践过程中,有很多我们一厢情愿地“安插”在软件中的热点。往往是费了力气去优化,性能反而下降了。当软件遭遇性能瓶颈的时候,要第一时间使用性能测试工具帮助你发现热点的真正所在。而对工具的运用和对测试结果的解释,是可以凭借经验的。

同时也应意识到,我们使用的硬件,编译我们软件的编译器等等也都是在不断进步的。很多我们自以为是的性能热点,都已经在我们看不到的地方得到了很好的解决。过去的瓶颈如今已不再,以过去的经验为基础所构筑的直觉自然也是靠不住的。

相信套路

很多时候我们会看到或者听到一些“万金油”式的性能优化方法。这里面最具代表性的三个就是prefetch、core affinity和hugepage。很多人不假思索地绑核,等性能测试结果出来,却成了一声棒喝。举例来说,绑的可能是两个不同的逻辑核心,但它们是不是在同一个物理核心上?所需要读取的内存是否在同一个NUMA节点上?是否会造成False sharing?所有这些都是需要以全局为考量,谋定而后动的。同理,很多人搞性能优化的第一件事就是上Hugepage和prefetch,不谋全局者不足以谋一隅,所遭遇的故事和绑核是一样的。

在后续的文章中,将会针对x86软件性能优化的各个方面进行细致地剖析。包括但不限于缓存、循环、分支、多线程、无锁,向量化等内容。

您还可以通过以下方式了解更多云杉网络的信息

%e9%bb%98%e8%ae%a4%e6%a0%87%e9%a2%98_%e5%85%ac%e4%bc%97%e5%8f%b7%e5%ba%95%e9%83%a8%e4%ba%8c%e7%bb%b4%e7%a0%81_2017-08-16-1

关注云杉网络公众号 yunshannetworks,回复“精选”查看;

%e5%85%ac%e4%bc%97%e5%8f%b7%e5%9b%be

云杉网络官方网站:yunshan.net

Related Posts

云杉网络 DeepFlow &必示RiskSeer应用性能智能监控预警方案

随着云原生技术的广泛应用,社会数字化快速发展,政府、金融、通信、电力、制造、消费等各行各业正在不断的被数字化、智能化改变,涌现出越来越多的大型、超大型 IT 业务。随之而来的是 IT 业务系统的复杂度越来越高,开发迭代速度越来越快,系统规模越来越大,运行风险越来越高,而业务抖动、业务故障的经济影响、社会影响也越来越广泛。 为了应对新的 IT 生产力带来的业务保障能力的挑战,可观测性技术快速发展和成熟,成为云原生时代公认的运维技术演进方向,可观测性平台也成为 IT 企业的必备选项。 可观测性技术从数据出发,致力于提升 IT 系统的可观察性、可维护性和运行可靠性,通过新的数据采集、数据处理、数据存储技术打通数据孤岛,形成百倍、千倍于上一代监控时代的数据体量,产生了 IT 运维的“数据大爆炸”。同时伴随着机器学习、神经网络、大模型等 AI 技术的不断爆发,使用 AI 技术对“数据大爆炸”产生的可观测性数据进行智能分析,逐步落地运维智能化将进一步改变 IT 运维,并最终实现端到端的 AI 运维保障能力。 通过在OpenAIOps 社区基于AIOps Live  Benchmark:https://www.aiops.cn/aiops-live-benchmark 进行充分验证,云杉网络与必示科技携手联合发布应用性能智能监控预警方案,融合云杉网络 DeepFlow 产品在可观测性、必示科技 RiskSeer 产品在运维数据 AI 分析的深厚技术积淀,实现 IT 系统高质量、高性能、全栈的可观测数据采集、智能监控和智能分析,全面提升云原生系统的可观测和智能化运维能力。 PART.01 方案架构 云杉网络 DeepFlow 可观测性平台,以 eBPF 零侵扰(Zero Code)观测数据采集技术实现的分布式追踪数据、应用调用性能数据、函数剖析数据为核心,以智能标签(SmartEncoding)技术实现的观测信号高性能关联和存取为支撑,以观测数据 Sink 接口实现的观测数据 Pipeline 为补充,面向复杂的云基础设施及云原生应用,实现了全栈、全链路的分布式追踪、应用性能指标分析、TCP 性能指标分析、持续性能剖析、网络流回溯等一系列的深度观测能力。平台通过高性能、高质量的数据采集和开放的数据汇入,形成了汇聚 Metrics、Trace、Logging、Profiling、Events 等观测信号的可观测性数据湖,湖内的各类观测信号数据通过自动注入的标签(资源标签、业务标签)高度关联并富含上下文信息。 必示科技 RiskSeer 产品基于大数据技术和时序基础模型,面向时序数据提供基于智能动态基线的指标监控预警能力,在趋势预测方面积累了丰富的数据样本和模型算法参数,具有模型算法通用性强、处理性能高、抗数据噪音、抗数据缺损、非周期变化自适应、周期漂移自适应等诸多优异的智能特性,帮助用户及时准确的发现系统运行异常、主动消除潜在风险,持续提升业务运行健康状态。 DeepFlow 与 […]

Read More

云杉网络可观测性服务快讯( 2024年8月)

01|某省电力公司 业务无响应 能源互联网营销服务系统集群某节点业务无响应,在 DeepFlow 中发现该节点的 TCP 连接都被服务端直接重置,根据经验判断是磁盘故障导致,通过 DeepFlow 的文件读写监控发现从故障时间点开始就没有磁盘写入操作,客户通知相关业务运维修复磁盘问题后业务恢复正常。 难度⭐️ 02 | 某互联网金融公司 关键服务监控视图 客户反馈 k8s 对外服务应用报了大量线程池满,无法对外提供服务。客户侧排查 1-2 小时后才确认具体出现故障的服务点。后续通过 DeepFlow 创建 k8s 对外服务监控视图,可以直观看到具体某个时间点,服务故障响应耗时的变化,快速排查故障点及故障原因。 难度⭐️ 03 | 某汽车企业 关键业务偶发超时 客户某注册中心业务长期存在低频偶发超时情况,经多次排查发现异常 reset 请求,但始终无法界定具体故障点。通过 DeepFlow 不同位置流日志,结合时序图快速发现,在问题发生时客户端容器节点处存在一个未经 SNAT 直接请求对端 Pod 的情况,并且 ACK  序列号完全相同,初步判断为容器 CNI 异常 BUG,客户立即通知云服务团队做进一步处理。 难度⭐️ 04 | 某汽车企业 业务流量治理与优化 客户成立流量治理团队,通过调用 DeepFlow 应用 RED 指标,嵌入 DeepFlow 业务拓扑图,利用 DeepFlow […]

Read More