在入口点标签之前带有指令的函数是否会导致任何问题(链接)?
Does a function with instructions before the entry-point label cause problems for anything (linking)?
这实际上是一个链接器/目标文件问题,但是用程序集标记,因为编译器从不这样做。 (尽管也许他们可以!)
考虑这个函数,我想用一个代码块处理一个特殊情况,该代码块与函数入口点位于同一 I-cache 行中。为了避免在通常的快速路径中跳过它,将它的代码放在函数的全局符号之前是否安全(关于链接/共享库/其他我没有想到的工具)?
我知道这很愚蠢/矫枉过正,见下文。大多数时候我只是好奇。不管这种技术是否有助于使代码在实践中实际运行得更快,我认为这是一个有趣的问题。
.globl __nextafter_pjc // double __nextafter_pjc(double x, double y)
.p2align 6 // unrealistic 64B alignment, just for the sake of argument
// GNU as local labels have the form .L...
.Lequal_or_unordered:
jp .Lunordered
movaps %xmm1, %xmm0 # ISO C11 requires returning y, not x. (matters for -0.0 == +0.0)
ret
######### Function entry point / global symbol here #############
// .p2align something // tuning for Sandybridge, maybe best to just leave this unaligned, since it's only 6B from the alignment boundary
nextafter_pjc:
ucomisd %xmm1, %xmm0
je .Lequal_or_unordered
xorps %xmm3, %xmm3
comisd %xmm3, %xmm0 // x==+/0.0 can be a special case: the sign bit may change
je .Lx_zero
movq %xmm0, %rax
... // some mostly-branchless bit-ninjutsu that I have no idea how I'd get gcc to emit from C
ret
.Lx_zero:
...
ret
.Lunordered:
...
ret
(顺便说一句,我正在搞乱 asm,因为 nextafter
because I was curious about how glibc implemented it. It turns out the current implementation 编译成一些带有大量分支的非常讨厌的代码。例如,检查 NaN 的两个输入应该使用 FP 比较来完成,因为那太棒了- 快速,尤其是在非 NaN 的情况下。)
在反汇编输出中,标签之前的指令被分组在前一个函数的指令之后。例如
0000000000400ad0 <frame_dummy>:
...
400af0: 5d pop %rbp
400af1: e9 7a ff ff ff jmpq 400a70 <register_tm_clones>
400af6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
400afd: 00 00 00
400b00: 7a 56 jp 400b58 <__nextafter_pjc+0x52>
400b02: 0f 28 c1 movaps %xmm1,%xmm0
400b05: c3 retq
0000000000400b06 <__nextafter_pjc>:
400b06: 66 0f 2e c1 ucomisd %xmm1,%xmm0
400b0a: 74 f4 je 400b00 <frame_dummy+0x30>
400b0c: 0f 57 db xorps %xmm3,%xmm3
400b0f: 66 0f 2f c3 comisd %xmm3,%xmm0
400b13: 74 4b je 400b60 <__nextafter_pjc+0x5a>
400b15: 66 48 0f 7e c0 movq %xmm0,%rax
...
请注意,主体中的第 4 条指令 comisd
从 400b0f
开始(并且未完全包含在包含函数入口点的第一个 16B 对齐块中) .因此,完全以这种方式进行指令获取和解码的 no-taken-branches 快速路径可能并不是真正的最佳选择。不过,这只是一个示例。
所以这个 似乎 可以工作,即使在文件的开头也是如此。它确实混淆了 objdump
,并且在 gdb
中并不理想(但这不是一个大问题)。 ELF 目标文件无论如何都不记录符号大小,因此 nm --print-size
无论如何都不做任何事情。 (nm --size-sort --print-size
,它试图计算符号大小,奇怪的是没有包括我的功能。)
我不太了解 Windows 目标文件。那里有更糟糕的事情发生吗?
我有点担心这里的正确性:有没有人试图通过从目标文件的符号地址中获取字节到下面的符号地址来从目标文件中复制单个函数?普通库存档(ar
用于静态库)和链接器复制整个目标文件,对吗?否则他们无法确定是否复制了所有必要的静态数据。
这个函数可能很少被调用,我们希望尽量减少缓存污染(I$、uop-cache、branch-predictors)。如果有的话,使用冷分支预测器针对未缓存的情况进行优化。
这可能很愚蠢,因为未缓存的情况很少发生。但是,如果 许多 函数都以这种方式优化,总的缓存占用空间将会减少,也许它们 将 都适合缓存。
请注意,最近的 Intel CPU 根本不进行静态分支预测,因此没有理由支持通常不采用的分支的前向分支。
我对 Agner Fog's microarch doc (the branch prediction chapter) is that they don't check whether a branch is "new" or not. They just use whatever entry is already in the BHT, without clearing it. This may not be exactly true, though, for Nehalem.
的理解是 "unknown" 不在 BHT 中的分支,而不是默认采用后向分支/不采用前向分支
有一种简单的方法可以让它看起来完全正常:在代码前面放置一个非全局标签。这使它看起来像(或者实际上 是 )一个 static
辅助函数。
非全局函数可以使用他们想要的任何调用约定相互调用。 C 编译器甚至可以通过 link-time/whole-program 优化来编写这样的代码,甚至可以只优化编译单元中的 static
函数。跳转(而不是调用)到另一个函数已经用于尾调用优化。
"helper function"代码可以在入口点以外的地方跳转到主函数。不过,我确信这对 link 用户来说不是问题。如果 linker 改变了 helper 和 main 函数之间的距离(通过在它们之间插入一些东西)而不调整跨越它扩大的差距的相对跳跃,那只会打破。我不认为任何 linker 会 首先以这种方式插入任何东西,并且在不修复任何分支的情况下这样做显然是一个错误。
我不确定在生成 .size
ELF 元数据时是否存在任何缺陷。我想我已经读到它对于将 linked 到共享库中的函数很重要。
以下内容适用于任何处理目标文件的工具:
.globl __nextafter_pjc // double __nextafter_pjc(double x, double y)
.p2align 6 // unrealistic 64B alignment, just for the sake of argument
nextafter_helper: # not a local label, but not .globl either
.Lequal_or_unordered:
jp .Lunordered
movaps %xmm1, %xmm0 # ISO C11 requires returning y, not x. (matters for -0.0 == +0.0)
ret
######### Function entry point / global symbol here #############
// .p2align something?
__nextafter_pjc:
ucomisd %xmm1, %xmm0
je .Lequal_or_unordered
...
ret
我们不需要 普通标签和 "local" 标签,但是针对不同的目的使用不同的标签意味着在重新安排事物时需要更少的修改。 (例如,您可以将 .Lequal_or_unordered
块放在其他地方,而无需将其重命名回 .L
并更改所有以它为目标的跳转。)nextafter_equal_or_unordered
可以作为单个名称使用。
这实际上是一个链接器/目标文件问题,但是用程序集标记,因为编译器从不这样做。 (尽管也许他们可以!)
考虑这个函数,我想用一个代码块处理一个特殊情况,该代码块与函数入口点位于同一 I-cache 行中。为了避免在通常的快速路径中跳过它,将它的代码放在函数的全局符号之前是否安全(关于链接/共享库/其他我没有想到的工具)?
我知道这很愚蠢/矫枉过正,见下文。大多数时候我只是好奇。不管这种技术是否有助于使代码在实践中实际运行得更快,我认为这是一个有趣的问题。
.globl __nextafter_pjc // double __nextafter_pjc(double x, double y)
.p2align 6 // unrealistic 64B alignment, just for the sake of argument
// GNU as local labels have the form .L...
.Lequal_or_unordered:
jp .Lunordered
movaps %xmm1, %xmm0 # ISO C11 requires returning y, not x. (matters for -0.0 == +0.0)
ret
######### Function entry point / global symbol here #############
// .p2align something // tuning for Sandybridge, maybe best to just leave this unaligned, since it's only 6B from the alignment boundary
nextafter_pjc:
ucomisd %xmm1, %xmm0
je .Lequal_or_unordered
xorps %xmm3, %xmm3
comisd %xmm3, %xmm0 // x==+/0.0 can be a special case: the sign bit may change
je .Lx_zero
movq %xmm0, %rax
... // some mostly-branchless bit-ninjutsu that I have no idea how I'd get gcc to emit from C
ret
.Lx_zero:
...
ret
.Lunordered:
...
ret
(顺便说一句,我正在搞乱 asm,因为 nextafter
because I was curious about how glibc implemented it. It turns out the current implementation 编译成一些带有大量分支的非常讨厌的代码。例如,检查 NaN 的两个输入应该使用 FP 比较来完成,因为那太棒了- 快速,尤其是在非 NaN 的情况下。)
在反汇编输出中,标签之前的指令被分组在前一个函数的指令之后。例如
0000000000400ad0 <frame_dummy>:
...
400af0: 5d pop %rbp
400af1: e9 7a ff ff ff jmpq 400a70 <register_tm_clones>
400af6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
400afd: 00 00 00
400b00: 7a 56 jp 400b58 <__nextafter_pjc+0x52>
400b02: 0f 28 c1 movaps %xmm1,%xmm0
400b05: c3 retq
0000000000400b06 <__nextafter_pjc>:
400b06: 66 0f 2e c1 ucomisd %xmm1,%xmm0
400b0a: 74 f4 je 400b00 <frame_dummy+0x30>
400b0c: 0f 57 db xorps %xmm3,%xmm3
400b0f: 66 0f 2f c3 comisd %xmm3,%xmm0
400b13: 74 4b je 400b60 <__nextafter_pjc+0x5a>
400b15: 66 48 0f 7e c0 movq %xmm0,%rax
...
请注意,主体中的第 4 条指令 comisd
从 400b0f
开始(并且未完全包含在包含函数入口点的第一个 16B 对齐块中) .因此,完全以这种方式进行指令获取和解码的 no-taken-branches 快速路径可能并不是真正的最佳选择。不过,这只是一个示例。
所以这个 似乎 可以工作,即使在文件的开头也是如此。它确实混淆了 objdump
,并且在 gdb
中并不理想(但这不是一个大问题)。 ELF 目标文件无论如何都不记录符号大小,因此 nm --print-size
无论如何都不做任何事情。 (nm --size-sort --print-size
,它试图计算符号大小,奇怪的是没有包括我的功能。)
我不太了解 Windows 目标文件。那里有更糟糕的事情发生吗?
我有点担心这里的正确性:有没有人试图通过从目标文件的符号地址中获取字节到下面的符号地址来从目标文件中复制单个函数?普通库存档(ar
用于静态库)和链接器复制整个目标文件,对吗?否则他们无法确定是否复制了所有必要的静态数据。
这个函数可能很少被调用,我们希望尽量减少缓存污染(I$、uop-cache、branch-predictors)。如果有的话,使用冷分支预测器针对未缓存的情况进行优化。
这可能很愚蠢,因为未缓存的情况很少发生。但是,如果 许多 函数都以这种方式优化,总的缓存占用空间将会减少,也许它们 将 都适合缓存。
请注意,最近的 Intel CPU 根本不进行静态分支预测,因此没有理由支持通常不采用的分支的前向分支。
我对 Agner Fog's microarch doc (the branch prediction chapter) is that they don't check whether a branch is "new" or not. They just use whatever entry is already in the BHT, without clearing it. This may not be exactly true, though, for Nehalem.
的理解是 "unknown" 不在 BHT 中的分支,而不是默认采用后向分支/不采用前向分支有一种简单的方法可以让它看起来完全正常:在代码前面放置一个非全局标签。这使它看起来像(或者实际上 是 )一个 static
辅助函数。
非全局函数可以使用他们想要的任何调用约定相互调用。 C 编译器甚至可以通过 link-time/whole-program 优化来编写这样的代码,甚至可以只优化编译单元中的 static
函数。跳转(而不是调用)到另一个函数已经用于尾调用优化。
"helper function"代码可以在入口点以外的地方跳转到主函数。不过,我确信这对 link 用户来说不是问题。如果 linker 改变了 helper 和 main 函数之间的距离(通过在它们之间插入一些东西)而不调整跨越它扩大的差距的相对跳跃,那只会打破。我不认为任何 linker 会 首先以这种方式插入任何东西,并且在不修复任何分支的情况下这样做显然是一个错误。
我不确定在生成 .size
ELF 元数据时是否存在任何缺陷。我想我已经读到它对于将 linked 到共享库中的函数很重要。
以下内容适用于任何处理目标文件的工具:
.globl __nextafter_pjc // double __nextafter_pjc(double x, double y)
.p2align 6 // unrealistic 64B alignment, just for the sake of argument
nextafter_helper: # not a local label, but not .globl either
.Lequal_or_unordered:
jp .Lunordered
movaps %xmm1, %xmm0 # ISO C11 requires returning y, not x. (matters for -0.0 == +0.0)
ret
######### Function entry point / global symbol here #############
// .p2align something?
__nextafter_pjc:
ucomisd %xmm1, %xmm0
je .Lequal_or_unordered
...
ret
我们不需要 普通标签和 "local" 标签,但是针对不同的目的使用不同的标签意味着在重新安排事物时需要更少的修改。 (例如,您可以将 .Lequal_or_unordered
块放在其他地方,而无需将其重命名回 .L
并更改所有以它为目标的跳转。)nextafter_equal_or_unordered
可以作为单个名称使用。