x86-64 汇编的性能优化 - 对齐和分支预测

2023-12-08

我目前正在编写一些 C99 标准库字符串函数的高度优化版本,例如strlen(), memset()等,使用带有 SSE-2 指令的 x86-64 汇编。

到目前为止,我已经在性能方面取得了出色的结果,但当我尝试进一步优化时,有时会出现奇怪的行为。

例如,添加甚至删除一些简单的指令,或者简单地重新组织一些与跳转一起使用的本地标签会完全降低整体性能。就代码而言,绝对没有理由。

所以我的猜测是,代码对齐和/或分支预测存在一些问题。

我知道,即使采用相同的架构(x86-64),不同的CPU也有不同的分支预测算法。

但是,在 x86-64 上进行高性能开发时,是否有一些关于代码对齐和分支预测的一般建议?

特别是,关于对齐,我是否应该确保跳转指令使用的所有标签都在 DWORD 上对齐?

_func:
    ; ... Some code ...
    test rax, rax
    jz   .label
    ; ... Some code ...
    ret
    .label:
        ; ... Some code ...
        ret

在前面的代码中,我应该在之前使用对齐指令吗.label:, like:

align 4
.label:

如果是这样,在使用 SSE-2 时对齐 DWORD 是否足够?

关于分支预测,是否有一种“首选”方式来组织跳转指令使用的标签,以帮助 CPU,或者当今的 CPU 是否足够智能,可以在运行时通过计算分支发生的次数来确定这一点?

EDIT

好的,这是一个具体的例子 - 这是开始strlen()使用 SSE-2:

_strlen64_sse2:
    mov         rsi,    rdi
    and         rdi,    -16
    pxor        xmm0,   xmm0
    pcmpeqb     xmm0,   [ rdi ]
    pmovmskb    rdx,    xmm0
    ; ...

使用 1000 个字符串运行 10'000'000 次大约需要 0.48 秒,这还不错。
但它不检查 NULL 字符串输入。显然,我将添加一个简单的检查:

_strlen64_sse2:
    test       rdi,    rdi
    jz          .null
    ; ...

同样的测试,现在运行时间为 0.59 秒。但是如果我在检查后对齐代码:

_strlen64_sse2:
    test       rdi,    rdi
    jz          .null
    align      8
    ; ...

原来的表演又回来了。我使用 8 进行对齐,因为 4 不会改变任何内容。
谁能解释一下这一点,并就何时对齐或不对齐代码部分提供一些建议?

EDIT 2

当然,这并不像对齐每个分支目标那么简单。如果我这样做,性能通常会变得更糟,除非像上面这样的某些特定情况。


对齐优化

1. Use .p2align <abs-expr> <abs-expr> <abs-expr>代替align.

使用 3 个参数进行细粒度控制

  • param1- 与什么边界对齐。
  • param2- 用什么(零或NOPs).
  • param3- 如果填充超出指定的字节数,则不对齐。

2. 将常用代码块的开头与缓存行大小边界对齐。

  • 这增加了整个代码块位于单个缓存行中的机会。一旦加载到 L1 高速缓存中,就可以完全运行,无需访问 RAM 来获取指令。这对于具有大量迭代的循环非常有益。

3.使用多字节NOPs 用于填充到减少执行时间NOPs.

  /* nop */
  static const char nop_1[] = { 0x90 };

  /* xchg %ax,%ax */
  static const char nop_2[] = { 0x66, 0x90 };

  /* nopl (%[re]ax) */
  static const char nop_3[] = { 0x0f, 0x1f, 0x00 };

  /* nopl 0(%[re]ax) */
  static const char nop_4[] = { 0x0f, 0x1f, 0x40, 0x00 };

  /* nopl 0(%[re]ax,%[re]ax,1) */
  static const char nop_5[] = { 0x0f, 0x1f, 0x44, 0x00, 0x00 };

  /* nopw 0(%[re]ax,%[re]ax,1) */
  static const char nop_6[] = { 0x66, 0x0f, 0x1f, 0x44, 0x00, 0x00 };

  /* nopl 0L(%[re]ax) */
  static const char nop_7[] = { 0x0f, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00 };

  /* nopl 0L(%[re]ax,%[re]ax,1) */
  static const char nop_8[] =
    { 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00};

  /* nopw 0L(%[re]ax,%[re]ax,1) */
  static const char nop_9[] =
    { 0x66, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00 };

  /* nopw %cs:0L(%[re]ax,%[re]ax,1) */
  static const char nop_10[] =
    { 0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00 };

(upto 10byte NOPs 代表 x86。来源binutils-2.2.3.)


分支预测优化

Lot of variations between x86_64 micro-architectures/generations. However a common set of guidelines that are applicable for all of them can be summarised as follows. Reference : Section 3 of Agner Fog's x86 micro-architecture manual.

1. 展开循环以避免迭代计数稍高。

  • 循环检测逻辑保证仅适用于具有< 64迭代。这是因为如果分支指令单向运行,则该分支指令将被识别为具有循环行为n-1次,然后走另一条路1时间,对于任何n最多 64 个。

    这并不真正适用于 Haswell 及更高版本中的预测器,它们使用 TAGE 预测器,并且没有针对特定分支的专用循环检测逻辑。在 Skylake 上,对于没有其他分支的紧密外循环内的内循环来说,迭代计数约为 23 可能是最坏的情况:内循环的退出大多数时候都会错误预测,但行程计数非常低,因此经常发生。展开可以通过缩短模式来提供帮助,但对于非常高的循环行程计数,最终的单个错误预测会在多次行程中摊销,并且需要不合理的展开量才能对此采取任何措施。

2.坚持近跳/短跳。

  • 无法预测远跳转,即管道总是在远跳转到新代码段 (CS:RIP) 时停止。无论如何,基本上没有理由使用远跳,所以这基本上是不相关的。

    在大多数 CPU 上,通常可以预测具有任意 64 位绝对地址的间接跳转。

    但是,当目标距离超过 4GB 时,Silvermont(Intel 的低功耗 CPU)在预测间接跳转方面存在一些限制,因此通过在低 32 位虚拟地址空间中加载/映射可执行文件和共享库可以避免这种情况的发生。 。例如在 GNU/Linux 上通过设置环境变量LD_PREFER_MAP_32BIT_EXEC。有关更多信息,请参阅英特尔的优化手册。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

x86-64 汇编的性能优化 - 对齐和分支预测 的相关文章

  • 为什么 Java 11 中对于空白字符串 String.strip() 比 String.trim() 快 5 倍

    我遇到过一个有趣的场景 因为某些原因strip 针对空白字符串 仅包含空格 明显快于trim 在Java 11中 基准 public class Test public static final String TEST STRING 3 w
  • 为什么对于小数组,for-of 循​​环比标准 for 循环快,而对于大数组则慢?

    在 JavaScript 中 我注意到 ES6for of循环的性能与传统的有很大不同for start stop step loop 基准 const n 10000 const arr Array n fill map e i gt i
  • 循环内的局部变量会被垃圾收集吗?

    我想知道将循环内引用的任何变量放在循环外是否更有效 或者它们可以像函数内的变量一样被垃圾收集吗 var obj key val for var i 0 i lt 10 i console log obj or for var i 0 i l
  • 优化数据可视化 Web 应用程序的性能

    我正在重写 3 年前编写的数据可视化网络工具 从那时起 浏览器的 JavaScript 引擎变得更快 所以我正在考虑将部分工作从服务器转移到客户端 在页面上 数据在表格和地图 或图表 中可视化 它使用相同的数据 但以不同的方式 因此准备显示
  • PhoneGap 1.4 封装 Sencha Touch 2.X - 性能怎么样?

    我正在构建一个多平台平板电脑应用程序 仅使用其 Webview 使用 Phonegap 1 4 对其进行包装 然后使用 Sencha Touch 2 框架发挥我的魔力 我所说的多平台是指 iOS 5 X 和 Android 3 0 目前 到
  • 是否可以在VM内使用VMX CPU指令?

    VM guest 内部的进程是否有可能使用 VMX AMD V VT x CPU 指令 然后由外部 VMM 处理而不是直接在 CPU 上处理 Edit 假设外部VM使用VMX本身来管理其虚拟客户机 即它在Ring 1中运行 如果可能的话 是
  • 在所有浏览器中启用我的网站的平滑滚动

    我正在开发一个视差滚动网站Stellar http markdalgleish com projects stellar js and Skrollr https github com Prinzhorn skrollr图书馆 该网站在 F
  • 在单个 mongodb 查询中查找并计数

    我的文档看起来像这样 id ObjectId 572c4bffd073dd581edae045 name What s New in PHP 7 description PHP 7 is the first new major versio
  • 页面上首次调用 Url.Action 速度很慢

    我有一个相当简单的 ASP MVC 视图的性能问题 这是一个登录页面 应该几乎是即时的 但需要大约半秒钟 经过大量挖掘后 问题似乎出在第一个调用上Url Action 大约需要 450 毫秒 根据迷你分析器 http miniprofile
  • JavaFX 中 WebView 的性能

    我有一个 HTML5 UI 和一个 Java 后端 并且希望避免在纯 java 中重建 HTML ui 所以我的想法是运行本地 Web 服务器并使用 WebView 在 本机 窗口中呈现它 解决方案似乎是使用可以嵌入到 swing 中的 J
  • 当我使用可变参数而不是常量参数时,为什么我的内联表 UDF 慢得多?

    我有一个表值内联 UDF 我想过滤该 UDF 的结果以获得一个特定值 当我使用常量参数指定过滤器时 一切都很好 并且性能几乎是瞬时的 当我使用可变参数指定过滤器时 它会花费明显更大的时间块 大约是逻辑读取的 500 倍和持续时间的 20 倍
  • python 日志记录会刷新每个日志吗?

    当我使用标准模块将日志写入文件时logging 每个日志会分别刷新到磁盘吗 例如 下面的代码会将日志刷新 10 次吗 logging basicConfig level logging DEBUG filename debug log fo
  • 为什么n++执行速度比n=n+1快?

    在C语言中 为什么n 执行速度快于n n 1 int n n int n n n 1 我们的老师在今天的课堂上问了这个问题 这不是家庭作业 如果您正在开发一个 石器时代 编译器 的情况下 石器时代 n比n 比n n 1 机器通常有incre
  • 独立滚动矩阵的行

    我有一个矩阵 准确地说 是 2d numpy ndarray A np array 4 0 0 1 2 3 0 0 5 我想滚动每一行A根据另一个数组中的滚动值独立地 r np array 2 0 1 也就是说 我想这样做 print np
  • linux perf:如何解释和查找热点

    我尝试了linux perf https perf wiki kernel org index php Main Page今天很实用 但在解释其结果时遇到了困难 我习惯了 valgrind 的 callgrind 这当然是与基于采样的 pe
  • NHibernate - CreateCriteria 与 CreateAlias

    假设以下场景 class Project public Job Job class Job public Name 假设我想使用 Criteria API 搜索其 Job 名称为 sumthing 的所有项目 我可以使用 CreateAli
  • 为什么X86中没有NAND、NOR和XNOR指令?

    它们是您可以在计算机上执行的最简单的 指令 之一 它们是我亲自实施的第一个指令 执行 NOT AND x y 会使执行时间和依赖链长度和代码大小加倍 BMI1 引入了 andnot 这是一个有意义的补充 是一个独特的操作 为什么不是这个问题
  • 分支预测器和分支目标缓冲区如何共存?

    我的问题是它们如何在现代 CPU 架构中共存并协同工作 你把它稍微颠倒了 每次获取时 您都会索引到分支预测器 它会告诉您刚刚收到的指令是否will be解码为已采取的分支 如果没有 则获取下一个连续地址 但是 如果您的分支预测器说它将是一个
  • movzbl(%rdi, %rcx, 1), %ecx 在 x86-64 汇编中意味着什么?

    我想我明白 movzbl rdi rcx 1 ecx 意思是 将零扩展字节移至长整型 并表示将 ecx 扩展为 32 位 但我不完全确定语法 rdi rcx 1 指的是什么 我在某处看到该语法指的是 Base Index Scale 但我找
  • 正则表达式库基准

    我最近一直想知道正则表达式实现的性能 并且很难想出很多有用的信息 它很容易对浏览器 javascript 正则表达式性能进行基准测试 网上有很多工具 Chrome 和 Opera 中的 javascript 正则表达式实现几乎摧毁了所有其他

随机推荐