【问题标题】:"Custom intrinsic" function for x64 instead of inline assembly possible?x64的“自定义内在”函数而不是内联汇编可能吗?
【发布时间】:2011-07-29 00:35:21
【问题描述】:

我目前正在尝试为我的库创建高度优化、可重用的函数。例如,我用以下方式编写函数“是 2 的幂”:

template<class IntType>  
inline bool is_power_of_two( const IntType x )
{
    return (x != 0) && ((x & (x - 1)) == 0);
}

这是一个可移植的、低维护的实现,作为内联 C++ 模板。这段代码由VC++ 2008编译成以下带有分支的代码:

is_power_of_two PROC
    test    rcx, rcx
    je  SHORT $LN3@is_power_o
    lea rax, QWORD PTR [rcx-1]
    test    rax, rcx
    jne SHORT $LN3@is_power_o
    mov al, 1
    ret 0
$LN3@is_power_o:
    xor al, al
    ret 0
is_power_of_two ENDP

我还从这里找到了实现:"The bit twiddler",它将在 x64 的汇编中编码如下:

is_power_of_two_fast PROC
    test rcx, rcx
    je  SHORT NotAPowerOfTwo
    lea rax, [rcx-1]
    and rax, rcx
    neg rax
    sbb rax, rax
    inc rax
    ret
NotAPowerOfTwo:
    xor rax, rax
    ret
is_power_of_two_fast ENDP

我在一个汇编模块(.asm 文件)中测试了与 C++ 分开编写的两个子例程,第二个的运行速度提高了大约 20%!

然而函数调用的开销是相当大的:如果我将第二个汇编实现“is_power_of_two_fast”与模板函数的内联版本进行比较,尽管有分支,后者更快!

不幸的是,x64 的新约定规定不允许内联汇编。应该改为使用“内在函数”。

现在的问题是:我可以将更快的版本“is_power_of_two_fast”实现为自定义内在函数或类似的东西,以便可以内联使用吗?或者,是否有可能以某种方式强制编译器生成函数的低分支版本?

【问题讨论】:

  • GCC 和 ICC 仍然允许内联汇编
  • 使用 & 代替 && 避免分支。
  • @drhirsch:谢谢,我记住了。 @Hans Passant:我已经尝试过了,但这会导致代码变慢(指令太多)。

标签: c++ assembly 64-bit inline-assembly intrinsics


【解决方案1】:

不,您不能实现任何自定义内在函数,它们都内置在编译器中。不仅是内置的指令,编译器也知道内在的语义,并为不同的周围代码调整代码。

为 x86-64 删除内联汇编的一个原因是,将汇编插入函数中间会干扰优化器,并且通常会导致围绕汇编代码的代码优化得不太好。那里很容易出现净亏损!

内在函数的唯一真正用途是编译器无法从 C 或 C++ 构造(如 BSF 或 BSR)生成的“有趣”特殊指令。使用内联函数(例如上面的模板),其他大多数事情都会更好地工作。

如果您需要做一些编译器不理解的特殊操作,唯一真正的选择是将整个函数编写为单独的汇编程序模块。如果该函数的调用开销太高,那么优化可能一开始就不值得。

相信你的编译器(tm)!

【讨论】:

    【解决方案2】:

    甚至 VC 2005 也能够使用 sbb 指令生成代码。

    C 代码

    bool __declspec(noinline) IsPowOf2(unsigned int a)
    {
        return (a>=1)&((a&(a-1))<1);
    }
    

    编译成如下

    00401000  lea         eax,[ecx-1] 
    00401003  and         eax,ecx 
    00401005  cmp         eax,1 
    00401008  sbb         eax,eax 
    0040100A  neg         eax  
    0040100C  cmp         ecx,1 
    0040100F  sbb         ecx,ecx 
    00401011  add         ecx,1 
    00401014  and         eax,ecx 
    00401016  ret          
    

    【讨论】:

    • 如果这是一个愚蠢的问题,我深表歉意,但是:__declspec(noinline) 不意味着它不能被编译器内联,需要函数调用的开销(OP 需要避免什么sbb 版本跑得更快)?
    【解决方案3】:

    VC10 x64 内在函数在这种简单情况下不会有很大帮助。 您拥有的动态分支归因于 && 运算符,它是一个早期输出运算符。 在许多情况下(您的案例是一个完美的例子),最好通过计算所有分支的结果来避免分支,然后应用掩码来选择好的分支。带有掩码的 cpp 代码如下所示:

    template<typename T_Type>
    inline bool isPowerOfTwo(T_Type const& x)
    {
        // static type checking for the example
        static_assert( std::is_integral<T_Type>::value && std::is_unsigned<T_Type>::value, "limited to unsigned types for the example" );
        typedef std::make_signed<T_Type>::type s_Type;
    
        // same as yours but with no branching
        return bool(  ((s_Type( s_Type(x != 0) << (s_Type(sizeof(T_Type)<<3u)-1) )) >> (s_Type(s_Type(sizeof(T_Type)<<3u)-1)))  & ((x & (x - 1)) == 0)  );
    }
    

    在上面的代码中,对于有符号类型,我没有检查数字是否为负数。同样,一个简单的掩码将通过执行算术右移 (numBit-1) 次来获得负数的 (~0) 值和正数的 0 值来解决问题

    【讨论】:

    • 不幸的是,您的建议与最初的 C++ 函数没有太大区别。带有汇编输出的编译表明 VC++2008 在编译代码时使用了“test”指令,并且分支仍然存在。
    【解决方案4】:

    前进的唯一方法是退后一步,开始放眼大局。要么停止实现微优化的 API,要么继续进行更大的 API 调用,所有这些都在 MASM64、YASM、NASM 等中进行了优化。

    如果您使用更强大的汇编器之一,您可以将小函数转换为宏,因此基本上将基于 C/C++ 标头的内联汇编器函数更改为汇编器包含文件。

    【讨论】:

      猜你喜欢
      • 2017-05-03
      • 2010-12-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-27
      相关资源
      最近更新 更多