【问题标题】:Embed assembler to manipulate 64-bit registers in portable C++嵌入汇编程序以在可移植 C++ 中操作 64 位寄存器
【发布时间】:2011-10-22 12:38:09
【问题描述】:

我有一个简单(但性能关键)的 C (嵌入在 C++ 中)算法来操作数据缓冲区......该算法“自然”使用 64 位大端寄存器值 - 我想优化这使用汇编程序来直接访问进位标志和 BSWAP,因此避免了一次操作一个字节的 64 位值。

我希望解决方案能够在操作系统/编译器之间移植 - 最低限度地支持 GNU g++ 和 Visual C++ - 以及分别在 Linux 和 Windows 之间移植。对于这两个平台,显然,我假设处理器支持 x86-64 指令集。

我找到了this document about inline assembler for MSVC/Windows,并通过 Google 找到了几个片段,详细说明了 g++ 的不兼容语法。我接受我可能需要在每种方言中单独实现此功能。我无法找到足够详细的关于语法/工具的文档来应对这一发展。

我正在寻找的是详细说明我可用的工具的清晰文档 - 包括 MS 和 GNU 工具集。虽然我多年前编写了一些 32 位汇编程序,但我已经生疏了 - 我会从汇编级别提供的简明文档中受益。

更复杂的是,我想使用 Visual C++ Express Edition 2010 为 Windows 编译...我知道这是一个 32 位编译器 - 但我想知道是否可以嵌入 64 位汇编成它的可执行文件?在我打算手工编码的部分,我只关心 64 位性能。

任何人都可以提供任何指针(请原谅双关语...)?

【问题讨论】:

  • 对不起,但我相信当您需要可移植代码时,请将其保留在 C++ 代码中。测试该代码会遇到很多问题。但是,对于 32 位和 64 位,它们是不同的可执行文件,因此您可以使用 #ifdef _M_IX86 ... 使用 32 位 c 代码 ... #else ... 使用您的 64 位代码 ... #endif。
  • 我还必须补充一点,现代编译器对代码进行了很多优化,不知道这是否真的值得付出努力,在考虑将其移植到其他语言之前,您应该进行一些性能测试。尝试使用 MSVC,然后检查是否值得。
  • 我确信我发现了一种情况,即访问 BSWAP 和进位标志将产生显着的好处。我很高兴实现多个版本(由预处理器指令保护) - 但我仍然需要建立可以在每个上下文中使用的语法。
  • 你在 MSVC 内部观看了吗?也许有你需要的东西。 msdn.microsoft.com/en-us/library/azcs88h2.aspx

标签: c++ assembly g++ x86-64 visual-c++-2010


【解决方案1】:

只是为了让您了解您前进道路上的障碍,这里有一个简单的内联汇编函数,有两种方言。首先,Borland C++ Builder 版本(我认为这也可以在 MSVC++ 下编译):

int BNASM_AddScalar (DWORD* result, DWORD x)
  {
  int carry = 0 ;
  __asm
    {
    mov     ebx,result
    xor     eax,eax
    mov     ecx,x
    add     [ebx],ecx
    adc     carry,eax    // Return the carry flag
    }
  return carry ;
  }

现在,g++ 版本:

int BNASM_AddScalar (DWORD* result, DWORD x)
  {
  int carry = 0 ;
  asm volatile (
"    addl    %%ecx,(%%edx)\n"
"    adcl    $0,%%eax\n"    // Return the carry flag
: "+a"(carry)         // Output (and input): carry in eax
: "d"(result), "c"(x) // Input: result in edx and x in ecx
) ;
  return carry ;
  }

如您所见,差异很大。而且没有办法绕过它们。这些来自我为 32 位环境编写的大型整数算术库。

至于在 32 位可执行文件中嵌入 64 位指令,我认为这是禁止的。据我了解,32位可执行文件在32位模式下运行,任何64位指令都会生成一个陷阱。

【讨论】:

  • 在 32 位可执行文件中禁止 64 位指令是有道理的,我怀疑你是对的。我仍然可以仅使用 32 位指令获得一些优势。到目前为止,您的示例在我的网上冲浪中很熟悉,您能指出描述这些语法的好文档吗?
  • 64 位指令在 32 位模式下大多会被解释为不同的指令,不一定无效,但很可能会导致崩溃。例如,REX 前缀的编码方式与 INC 和 DEC 相同。
  • @aSteve:GnuCompiler Collection 文档在这里:gcc.gnu.org/onlinedocs/gcc。 6.41 和 6.42 节是你想要的。不幸的是,文档是相当无情的——你需要成为一名 GNU 编译器编写者(我不是),或者你需要做很多实验(我做过)。
【解决方案2】:

不幸的是,MSVC++ 不支持 64 位代码中的内联汇编,也不支持 __emit。使用 MSVC++,您应该在单独的 .asm 文件中实现代码片段并编译它们并将它们与其余代码链接,或者诉诸如下肮脏的黑客攻击(为 32 位代码实现概念证明):

#include <windows.h>
#include <stdio.h>

unsigned char BswapData[] =
{
  0x0F, 0xC9, // bswap ecx
  0x89, 0xC8, // mov   eax, ecx
  0xC3        // ret
};

unsigned long (__fastcall *Bswap)(unsigned long) =
  (unsigned long (__fastcall *)(unsigned long))BswapData;

int main(void)
{
  DWORD dummy;
  VirtualProtect(BswapData, sizeof(BswapData), PAGE_EXECUTE_READWRITE, &dummy);
  printf("0x%lX\n", Bswap(0x10203040));
  return 0;
}

输出:0x40302010

我认为您不仅应该能够使用 gcc,而且 Linux 也应该能够做到这一点,但大约有两个细微差别(VirtualProtect() 是一个,调用约定是另一个)。

编辑:以下是在 Windows 上以 64 位模式对 64 位值执行 BSWAP 的方法(未经测试):

unsigned char BswapData64[] =
{
  0x48, 0x0F, 0xC9, // bswap rcx
  0x48, 0x89, 0xC8, // mov   rax, rcx
  0xC3              // ret
};

unsigned long long (*Bswap64)(unsigned long long) =
  (unsigned long long (*)(unsigned long long))BswapData64;

其余的都是微不足道的。

【讨论】:

  • 这太脏了! (不过很有趣……而且我很好奇参数如何映射到 C++ 或从 C++ 映射。)我怀疑我会对单独的 .asm 文件最满意……如果我知道编写它们的语法。有没有什么好的交叉汇编工具可以帮助我写这个“一次”?您是否有详细说明调用约定以及它们如何与单独的 .asm 方法交互的参考资料?
  • @aSteve:像往常一样,MSDN 是您的 MSVC++ 朋友。特别是 here 用于 64 位代码。 Wikipedia 链接到this doc,用于 Linux 64 位调用约定。至于编写一次代码...看看您是否可以获得适用于 MSVC++ 的 NASM/FASM 输出 64 位目标代码。 NASM/FASM 应该与 gcc 一起使用。
【解决方案3】:

有许多函数可用于交换字节顺序,例如从 BSD 套接字:

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

64 位的可移植性较差:

unsigned __int64 _byteswap_uint64(unsigned __int64); // Visual C++
int64_t __builtin_bswap64 (int64_t x). // GCC

不要每次在标准 C++ 中无法表达某些东西时都求助于汇编。

【讨论】:

    【解决方案4】:

    根据定义,C 或 C++ 中的 asm 语句不可移植,特别是因为它们与特定指令集相关联。特别是,如果您的汇编语句适用于 x86,请不要期望您的代码在 ARM 上运行。

    此外,即使在像 64 位 x86-64(即现代 PC)这样的相同硬件平台上,不同的系统(例如 Linux 与 Windows)也有不同的汇编语法和不同的调用约定。所以你的代码应该有几个变体。

    如果使用 GCC,它会为您提供许多可以帮助您的内置函数。并且可能(假设最近的 GCC,即 4.6 版本),它能够非常有效地优化您的功能。

    如果性能非常重要,并且您的系统有 GPU(即功能强大的显卡),您可能会考虑在 OpenCL 或 CUDA 中重新编码数值内核。

    【讨论】:

    • 汇编代码的几种变体是可以接受的...我需要的是很好的参考,描述了我应该为每个变体使用的语法。我对 CUDA 和 OpenCL 都感兴趣——它们代表了潜在的未来增强功能……尽管我怀疑它们可能会因为我的算法本质上是非并行的这一事实而失败。目前,我只对高效汇编级实现的优势感兴趣。
    【解决方案5】:

    内联汇编器不是您的选择之一:Win64 Visual C 编译器不支持__asm,您需要使用单独的 [m|y|n]asm 编译文件。

    【讨论】:

    • 你能给我指出 [m|y|n]asm 语法的参考吗?一个比另一个更便携吗?
    • aSteve:谷歌是你的朋友,文档丰富。我没有这方面的经验,所以我不能明智地回答你的问题。
    • 作为一名狂热的 Google 员工,我的大部分问题是文档“丰富”。 :) 我目前正在研究 yasm - 它看起来很现代,所以我很有希望......来自实现类似功能的人的特定指针将非常有帮助。 :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-01-05
    • 1970-01-01
    • 1970-01-01
    • 2011-01-31
    • 1970-01-01
    • 2016-11-11
    • 1970-01-01
    相关资源
    最近更新 更多