【问题标题】:Write x86 asm functions portably (win/linux/osx), without a build-depend on yasm/nasm?可移植地编写 x86 asm 函数(win/linux/osx),而不依赖于 yasm/nasm?
【发布时间】:2015-07-30 21:20:10
【问题描述】:

par2 有一个小而相当干净的 C++ 代码库,我认为它可以很好地在 GNU/Linux、OS X 和 Windows(使用 MSVC++)上构建。

我想合并一个函数的 x86-64 asm 版本,该函数几乎占用所有 CPU 时间。 (mailing list posts with more details。我的implementation/benchmark here。)

Intrinsics 是显而易见的解决方案,但 gcc 无法生成足够好的代码来从 64 位寄存器一次获取一个字节以用作 LUT 的索引。我也可能会花时间安排指令,以便每个 uop 缓存行包含 4 个 uop 的倍数,因为即使输入/输出缓冲区大小合适,uop 吞吐量也是瓶颈。

我不想在 yasm 上引入构建依赖项,因为很多人安装了 gcc,但没有安装 yasm。

有没有办法在一个单独的文件中用 asm 编写一个函数,供 gcc/clang 和 MSVC 组装? 目标是:

  • 没有额外的软件作为构建依赖。 (没有 YASM)。
  • 每个 asm 函数只有一个版本。 (不维护相同代码的 MASM 和 AT&T 版本。)

Par2cmdline 的构建系统对于 Unix 是 autoconf/automake,对于 Windows 是 MSVC .sln

我知道 GNU assemble 有一个 .intel_syntax noprefix 指令,但这只会改变指令格式,不会改变其他汇编程序指令。例如.align 16align 16。我的代码相当简单而且很小,所以如果可以的话,可以使用 C 预处理器 #defines 来解决不同的指令。

我假设在 C++ 中进行 CPU 检测并根据结果设置函数指针应该不是问题,即使我必须为此使用一些 #ifdef 条件编译。

如果没有我所希望的解决方案,我可能会引入一个依赖于 yasm 的构建,并有一个 ./configure --no-asm 选项来禁用在没有 yasm 的 x86 上构建的人的 asm 加速。

处理 Windows 和 Linux ABI 中不同调用约定的首选计划是在我的 C 原型上为我的 asm 函数使用 __attribute__((sysv_abi))。然后我只需要为 SysV ABI 编写函数序言。 MSVC 是否有类似的东西,可以根据某些功能的 SysV ABI 将 args 放入 regs 中? (顺便说一句,这让compiler bug 发痒了,所以如果你想让你的代码与当前的 gcc 一起工作,请小心这个想法。)

【问题讨论】:

  • XY 问题 - gcc 无法生成足够好的代码,无法从 64 位寄存器一次获取一个字节用作 LUT 的索引 你真的应该问这个并提供代码。
  • @Jester:我在我添加的新的最后一段中链接的那个 gcc 错误报告中跑题了。如果你想看看。我真的应该单独报告该性能错误。其实只要编译github.com/pcordes/par2-asm-experiments/blob/master/…
  • 我认为这根本不可能。所以我会选择 YASM,人们可以处理它。这不是他们第一次必须安装一些东西来构建一些软件。
  • 我认为按照你的建议去做是不切实际的。此外,YASM 可能是第四个最有可能安装在某人机器上的汇编程序。使用 GNU 汇编器(甚至 MASM)会更有意义,因为您会知道至少有一些用户需要安装。最后,不,没有办法告诉 MSVC 使用 System V 调用约定。为什么会有?它不针对任何使用它的系统。使用 Microsoft ABI 会更有意义,因为 GCC 和 clang 确实支持它。
  • @Jester:我不再偷懒,报告gcc问题:gcc.gnu.org/bugzilla/show_bug.cgi?id=67072

标签: visual-c++ gcc assembly x86 sse


【解决方案1】:

虽然我没有很好的解决方案来消除对特定汇编程序的依赖,但我确实有关于如何处理两种不同的 64 位调用约定的建议:Microsoft x64 与 SysV ABI。

最小的公分母是 Microsoft x64 调用约定,因为它只能通过寄存器传递前四个值。因此,如果您限制自己并使用宏来定义寄存器,您可以轻松地使您的代码同时编译为 Unix (Linux/BSD/OSX) 和 Windows。

例如在 Agner Fog 的 asmlib 中查看文件 strcat64.asm

%IFDEF  WINDOWS
%define Rpar1   rcx                    ; function parameter 1
%define Rpar2   rdx                    ; function parameter 2
%define Rpar3   r8                     ; function parameter 3
%ENDIF
%IFDEF  UNIX
%define Rpar1   rdi                    ; function parameter 1
%define Rpar2   rsi                    ; function parameter 2
%define Rpar3   rdx                    ; function parameter 3
%ENDIF

        push    Rpar1                  ; dest
        push    Rpar2                  ; src
        call    A_strlen               ; length of dest
        push    rax                    ; strlen(dest)
        mov     Rpar1, [rsp+8]         ; src
        call    A_strlen               ; length of src
        pop     Rpar1                  ; strlen(dest)
        pop     Rpar2                  ; src
        add     Rpar1, [rsp]           ; dest + strlen(dest)
        lea     Rpar3, [rax+1]         ; strlen(src)+1
        call    A_memcpy               ; copy
        pop     rax                    ; return dest
        ret

;A_strcat ENDP

我不认为四个寄存器真的是一个限制,因为如果你在汇编中编写一些东西,那是因为你想要最好的效率,在这种情况下,与函数本身相比,函数调用开销应该可以忽略不计,所以推送/弹出一些如果您在调用函数时需要向/从堆栈取值,则不会对性能产生影响。

【讨论】:

  • 是的,par2 用例每次调用都会花费很长时间,而且它也是一个叶函数,所以我不需要求助于 stack-args ABI。不过,我必须为每个 ABI 定义 5 个宏,以涵盖所有规则。 (否则,如果 rdi 可能与 Rpar1 相同的寄存器,我不能将它用作临时寄存器。) regs 的宏名称实际上应该使我的代码更具可读性,因为我试图保持序言很小以减少代码占用,所以我将 rdi 用于 dest 缓冲区以外的其他内容,以及类似的内容。
  • @PeterCordes,你指的是哪五个 ABI?
  • 对于两个 ABI,我都需要两个 ABI 使用的所有寄存器的符号名称宏。无论如何,寄存器的符号宏名称是一个好主意,因为我使用rdi 来表示 dest 缓冲区以外的东西,以及类似的东西。我觉得这会损害代码的人类可读性,但我讨厌为此牺牲函数序言中的 insn 字节(并且在任何地方都需要额外的 rex 前缀)。
  • @PeterCordes,我希望你不介意这个评论,你的许多答案和 cmets 让我想起了 Pascal 的一句话“我很抱歉不得不给你写这么长的信,但是我没有时间给你写一个简短的”。
  • @PeterCordes,没关系,你的写作错误仍然比我少(看看我回答中的连续句子)。我认为 SO 上的许多人都会通过图灵测试(尤其是许多 C++ 纯粹主义者)。这很有趣,因为我认为几年后他们的计算机就会通过测试。这将成为讽刺的新典范。
猜你喜欢
  • 2014-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-21
  • 1970-01-01
  • 2019-02-02
  • 2011-03-21
  • 1970-01-01
相关资源
最近更新 更多