【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

强者恒强:x86 高性能编程笺注之分支

  • 2017-05-22
  • 本文字数:4635 字

    阅读完需:约 15 分钟

读者须知:《强者恒强:x86 高性能编程笺注》是云杉网络推出的系列技术分享,该系列文章将分享 x86 高性能开发方面的实践和思考。主要内容目录如下,欢迎各位业界同仁与我们讨论交流相关话题。

  • 第一部分:介绍
    • 寻找软件中的 Hotspot
    • x86 CPU 架构
    • Good/Bad examples
  • 第二部分:性能因素
    • 内存
      • 缓存
      • 数据对齐
      • Prefetch
      • NUMA
      • 大页
      • 实例
    • 循环
      • 数据依赖
      • 循环展开
      • pointer aliasing
      • 实例
    • 分支
      • 流水线
      • 分支预测
      • Branch-less 编程
      • 实例
    • 多线程
      • 锁 / 阻塞
      • CPU 核绑定
      • 无锁操作
  • 附:
    • 性能测试工具

序言

以流水线的眼光来看,分支并不是高速公路上两个目的地的选择,如果预测失败,将是直接拐向出口处的收费站,还是不带 ETC 的。越是高级的流水线,受分支的影响也就越深。我们在观察各种性能测试工具提供给我们的结果的时候,经常会看到“stalled”这个单词,这个词与“发动机”这类词一起连用的时候,就是“熄火”的意思。而 stalled-cycles-frontend,作为一个衡量流水线前端 (Fetch & Decode) 运行水平的指标,与错误的分支预测 (Branch Misprediction) 有密切的关系。

有很多办法可以帮助我们优化分支,除了套用一个 likely()/unlikely() 这种熟知的方式之外,还有很多需要深入了解的知识。想尽量消除分支对性能的影响,先从了解分支开始吧。

分支的类型

分支分为条件分支 (Conditional Branch) 和非条件分支 (Unconditional Branch)。可以很容易的从字义区分:

条件分支对性能的影响最为显著。从前面一节关于流水线的描述中我们已能得知,在判断条件的指令没有经过执行 (EXE) 阶段之前,是无法确定正确的分支的。但等待显然不是最优的方案,CPU 还是会继续选取某一个分支的指令继续送入流水线。当发现这一分支是错误分支的时候,流水线不得不停止所有现有的工作,清空错误分支的指令,并从分支正确的地方重新开始,性能的损失也就不可避免。

对于非条件分支,也需要注意除了 goto 这种专门的转跳指令之外,调用某一个函数,或是函数指针,以及从子函数中返回也都属于非条件分支的范畴。

分支优化的原则

简单来说,就是靠猜。当然,在学术上叫“预测”,其实叫“看人品”也没错。如果能一直猜对正确的分支,那么这部分代码运行起来和无分支代码也没什么区别。这个工作主要由 CPU 架构中的分支预测单元 (Branch Prediction Unit, BPU 或 Branch Predictor) 完成。不过呢,BPU 不掷骰子,所有的预测都是基于历史结果的记录和分析。

Side Note: 分支预测 (Branch Prediction) 和分支目标预测 (Branch Target Prediction) 有区别。分支预测是在确定(条件)分支之前判断哪一分支更有可能;分支目标预测是在确定某一条件分支或无条件分支之后预测其转跳目标以便预取。不过这两者通常由同一处 CPU 电路完成。

在这里简单地讲一下分支预测的原理。2-bit Saturating Counter 是一种古老的技术,但作为现代分支预测器之滥觞,仍然值得我们分析一下。

如下图所示的状态机,共存在 4 种状态。其状态转换的条件,是本次该分支是否被选择。如果以 1 代表被选择 (taken),0 代表没有 (not taken),那么 1 向右边移动,0 向左边移动。当该分支确实是大概率转跳的分支 (taken),那么状态会大部分时间维持在 strongly taken 附近,如此可为下一次的预测提供预测。反之,则状态会维持在 strongly not taken 附近,也可以作为预测的凭据。

Side Note: 各个分支的 Saturating Counter 组织在一个表中,并以指令的地址作为 index,那么 CPU 就可以在 Fetch 阶段取得该指令的 Saturating Counter 状态。

Saturating Counter 确实帮助我们解决了一部分问题。但这种机制也存在缺陷。它对按某种规律重复出现的分支处理结果缺乏良好的支持,而这种情况在我们的程序中也经常出现,考虑如下代码:

复制代码
for (i = 0; i < 1000; i++) {
if (i & 1) { /* If i is odd num */
count++;
}
}

奇数总是间隔出现,if 的分支也是每隔一次循环执行一次。如果继续采用如上的方式,那么 Saturating Counter 总是会在两个相邻状态之间摇摆,并且总是提供错误的预测。对此,又引入了二级自适应预测器 (Two-level adaptive predictor)。

该机制可以理解为,为一个分支分配多个 Saturating Counter,以历史的情况决定使用哪一个 Saturating Counter。以上面的代码举例,if 分支共分配有 4 个 Saturating Counter,Table Entry Index 分别为 00,01,10 和 11。由该分支的最后两次选取结果决定使用哪一个 Saturating Counter。在上面的代码中,选取结果为 01010101010…当最后两次结果为 01 的时候,Index 01 的 Saturating Counter 接受本次的选取结果 0,并向 Strongly not taken 移动且维持该状态;当最后两次结果为 10 的时候,Index 10 的 Saturating Counter 接受本次的选取结果 1,并向 Strongly taken 移动且维持该状态。故当 10 这一选取规律再次出现的时候,对应的 Strongly taken 状态便可帮助 CPU 预测本次该分支将会被选取。如此,便可以为按某一规律出现的分支提供预测。

从上面的分析中可以得出,分支优化的原则就是让程序中分支的选取更加规律,这是一件十分合分支预测器胃口的事情。比如我们在第一节中所举的例子,对输入的随机数据首先排序再进行判断,就是利用的这一原则。但分支优化的最重要的原则,并不是这一条,而是在能不用分支的时候,尽量不要用分支。比如将上面的代码改成:

复制代码
for (i = 0; i < 1000; i++) { /* Ignore the optimization of loop*/
count += (i & 1);
}

性能相关的分支类型

从性能优化的角度来看,程序中的分支大致可分为如下几类:

第一次执行的条件分支

所谓“第一次”执行,可以真的是第一次,也可以是因为缓存更新等原因导致分支预测器中没有保存其历史数据。因为是第一次执行,所以很难对分支的情况作出有效的预测。例如 C 语言中的 if,while,for 以及汇编中的 jz,jne 等指令,CPU 虽然在这种情况下也会有一套执行的标准,但这取决于不同 CPU 的不同实现。除了使用 likely()/unlikely() 人为标注之外,还可以使用一些消除分支的通用方法。但这种情况非常罕见,优化的效果也难尽如人意。对于分支优化,和其他所有的优化一样,首要是抓住经常多次执行的关键代码段,以期取得优化效果的最大化。

重复执行的分支

重复执行的分支可以理解为分支预测器中存有其相关历史的分支。分支预测器能够以历史为依据,对下一次的分支选择做出判断。对这一类型的分支,可以用测试工具查看其 Branch Misprediction Rate。程序员需要做的,是尽可能让此类分支呈现出规律性,消除随机性带来的负面影响。现代的分支预测器已经远远比上文中介绍的复杂,发现分支中隐藏的规律并不是一个有特别难度的问题,当前,前提是规律真的存在:D

间接转跳分支

除了 if,while 之外还有利用函数指针或者 Jump Table 进行的间接转跳分支。间接转跳与 if while 这类转跳的不同之处在于,其转跳目标 (Branch Targets) 存在无限多种可能(函数指针可指向任意函数)。而 if while 只存在两种可能:下一条指令,或者转跳分支的第一条指令。现代的 CPU 同样对分支目标准备了预测机制,但和分支预测类似,该机制也同样依赖于“规律”二字。

非条件直接转跳分支

此类分支属于最少让人操心的分支。分支预测,100% 正确,分支目标预测 100% 正确,很难引发性能下降。

分支优化的技术

使用 Setcc 及 CMOVx 指令

使用 SETcc 及 CMOVx 指令可以帮助消除分支。不过这其实是一种类似于“空间换时间”的交换。这两个指令消除了分支在指令流程上的依赖,但引入了数据依赖,并将进一步影响乱序执行核心 (OOO Core) 的操作。并且这两条指令相当于将两个分支都执行了一次,对于可以预测的分支,一定不要采取这种技术。

Side Notes: 现代 x86 CPU 往往拥有很高的分支预测正确率。持续的无法预测的分支是比较罕见的情况。对于是否采用这两个指令,一定要以实际测试数据为依据。

  • SETcc

举一个经典的例子:

复制代码
R = (A < B) ? NUM1 : NUM2;

R 最终的取值取决于 A 与 B 相较的结果。当 A 与 B 之间无甚关联的时候,CPU 很难预测结果。该条指令比较“正统”的汇编指令如下:

复制代码
cmp a, b ; Condition
jbe L30 ; Conditional branch
mov ebx num1 ; ebx holds R
jmp L31 ; Unconditional branch
L30:
mov ebx, num2
L31:

在 jbe L30 处产生分支,最终 ebx 的取值,取决于 A 与 B 的比较结果。使用 SETcc 指令可以用如下的方式消除分支:

复制代码
xor ebx, ebx ; Clear ebx (R in the C code)
cmp A, B
setge bl ; When ebx = 0 or 1
; OR the complement condition
sub ebx, 1 ; ebx=11...11 or 00...00
and ebx, NUM3 ; NUM3 = NUM1-NUM2
add ebx, NUM2 ; ebx=NUM1 or NUM2

setge 并没有真正避免 A 与 B 的比较,而是依据比较结果修改了寄存器 ebx 的值。之后又利用了补码的一个数学运算上的小技巧,消除了分支。

  • CMOVx

CMOVx 所采用的思路,是将两种可能的结果都先计算出来,然后根据条件判断结果 (EFLAGS) 决定采用哪一结果。

考虑如下代码:

复制代码
if (x < 0) {
x = -x;
}
mov edx, eax ; 假设 x 的值在 eax 寄存器, 该指令使 edx=eax
neg eax ;eax=-eax 该指令的结果将设置条件寄存器的状态
cmovs eax,edx ; 如果状态为负, 将 edx 的值传递给 eax

这两个指令表面看来不存在分支,但其实是将分支判断隐藏在了对条件寄存器(EFLAGS) 的状态判断中。只不过这些指令的判断有 CPU 硬件电路支持,所以和通常意义的软件分支判断有所区别。当分支预测正确率不理想的时候,采用这两个指令会带来一定的性能提升。

使用 likely()/unlikely()

likely()/unlikely() 是最常见的帮助分支预测的方法。其为 GCC 编译器提供的两个内建函数(其他编译器也有类似的功能)的宏:

复制代码
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

程序员可以通过这两个宏将分支预测的信息提供给编译器,由编译器根据 CPU 执行代码的标准生成汇编代码,将可能转跳 (likely()) 或可能不转跳 (unlikely()) 的分支的代码放置于合适的位置,以达到人为提供分支预测信息的目的。这两个语句很常见,只要是稍微有点性能优化的意识,就会用到这两个宏。比较好奇的情况是,当实际运行情况与程序员标注的情况相左时,CPU 是否会自己纠正?这就要留给读者自己动手去验证了:D

使用无分支代码

还记得分支优化的第一原则吗?就是尽量消除分支。SETcc 和 CMOVx 指令是一种消除分支的方法,但也还有很多其他的方法。例如我们在第一节所举的例子:

复制代码
if (data[i] > 128) {
sum += data[i];
}

对于影响分支预测的随机输入,除了先排序之外,当我们想采用 CMOVx 指令的时候,可以写为:

复制代码
int t = data[i];
sum += (t > 128) ? t : 0;

CMOVx 指令固然可以消除分支,但并非是没有代价。当我们想避免这种代价的时候,还可以使用如下无分支代码 (Branch-less Code) 操作:

复制代码
int t = (data[i] - 128) >> 31;
sum += ~t & data[i];

无分支的代码是一种编码风格,虽然它彻底消除了分支对 CPU 流水线的影响,但它也存在增加计算量,降低代码可读性的问题。现代 CPU 的分支预测正确率已经可以在一般情况下维持在 95% 以上,所以当分支存在可预测的规律的时候,还是以性能测试的结果为最终的优化依据。

作者简介

张攀,云杉网络工程师,专注于 x86 网络软件的开发与性能优化,深度参与 ONF/OPNFV/ONOS 等组织及社区,曾任 ONF 测试工作组副主席。

2017-05-22 17:492212

评论

发布
暂无评论
发现更多内容

海外HTTP代理哪家最好用?Rola-IP与StormProxies的全方位数据对比

Geek_bf375d

ROLA-IP海外IP代理为第四届全球跨境电子商务大会注入活力

Geek_bf375d

macOS 14 Sonoma 14.1.1正式版(最新苹果系统) pkg完整安装包

Rose

苹果系统 macOS 14 Sonoma Mac14系统

Mac 版截图工具链

Eric 老乌龟

macos 工具

瓴羊X阿里云上的Salesforce联合解决方案正式发布

ToB行业头条

“箭在弦上”的边缘计算,更需要冷静和智慧

脑极体

服务器

一物一码需求,标签制作功能轻松解决

草料二维码

二维码 二维码生成 标签制作 一物一码

销售易取得500强客户背后的实践与进化

B Impact

Barcode for Mac:快速生成各类条形码

Rose

mac软件下载 条形码设计 Barcode for Mac Barcode 下载

Linux cat命令

二哈侠

软件测试/测试开发丨性能测试体系学习笔记

测试人

软件测试

聊聊低代码技术

互联网工科生

软件开发 低代码

哪些行业发展需要用到代理IP?罗拉ROLA-IP告诉你什么是专业

Geek_bf375d

投资机构Janus Capital Group为Rola-IP品牌融资700万美元

Geek_bf375d

Python 机器学习入门:数据集、数据类型和统计学

小万哥

Python 程序员 软件 后端 开发

剑指数据结构—实现动态数组

少年游侠客

数据结构 数组 ArrayList Java’

华为云开源 | 线下meetup · 电子科技大学站圆满收官

华为云开源

云原生 开源项目 开源社区

【Data & AI Con Shanghai 2023】嘉宾专访|西电王皓:认清边界 大胆创新

白玉兰开源

人工智能 白玉兰开源

Mac矢量图设计Sketch for Mac v99中文激活版 永久使用

Rose

sketch Mac Mac矢量图设计 Sketch 99新功能 Sketch 中文版下载

荣誉 | 观测云登榜「2023 中国好 SaaS TOP 10 SaaS 企业 」

观测云

可观测性 SaaS

IP代理哪家好用? 必看经典文

Geek_bf375d

8款好用的笔记软件,让你的读书笔记独一无二!

彭宏豪95

读书笔记 效率 软件推荐 在线白板 笔记软件

云电脑与5G网络的结合将会带来什么

青椒云云电脑

云电脑

低代码工具的常见用例与受众市场

树上有只程序猿

低代码

分布式AI在LLM时代的技术深度探索

不在线第一只蜗牛

人工智能 AI lee

Unity中国全面支持OpenHarmony游戏开发,多款游戏率先完成适配

最新动态

海外IP代理rola-ip表现突出,全球覆盖面广,技术支持优秀

Geek_bf375d

喜报 | MIAOYUN通过2023年度四川省“专精特新”中小企业认定!

MIAOYUN

专精特新 MIAOYUN 高新技术企业 专精特新中小企业 专精特新企业

领跑同一阵营!百分点科技入选Forrester AI/ML权威报告

百分点科技技术团队

人工智能 数据科学 百分点科技

Go类型嵌入介绍和使用类型嵌入模拟实现“继承”

快乐非自愿限量之名

Go 编程 教程 语言 教程分享

强者恒强:x86高性能编程笺注之分支_语言 & 开发_张攀_InfoQ精选文章