没有相应编译器标志的Clang/GCC编译器内部函数

8
我知道有类似的问题,但是编译具有不同标志的不同文件在这里不是可接受的解决方案,因为它会使代码库变得非常复杂。如果回答“不,不可能”的话就行了。
在任何版本的Clang或GCC中,是否可以仅启用编译器使用SSE指令集进行优化,同时为SSE 2/3/3S/4.1编译内部函数? 编辑:例如,我希望编译器将_mm_load_si128()转换为movdqa,但编译器必须不对除此内部函数以外的任何地方发出此指令,类似于MSVC编译器的工作方式。 编辑2:我已经准备好了动态调度程序和使用内部函数编写的具有不同指令集的单个功能的多个版本。使用多个文件将使维护变得更加困难,因为相同版本的代码将跨越多个文件,而且有很多这种类型的函数。 编辑3:请求示例源代码:https://github.com/AviSynthPlus/blob/master/avs_core/filters/resample.cpp或该文件夹中的大多数文件。

1
这并不十分清楚。你想将SSE2+内部函数自动编译为SSE1吗? - Oliver Charlesworth
1
使用不同的编译器标志会使代码变得更加复杂吗? - user529758
你可能需要调整所使用的ISA,例如使用-mtune=native标志来编译gcc。你同意吗?你可能想要在构建器方面投入更多的努力(例如为最新版本的make编写复杂的Makefile)。 - Basile Starynkevitch
1
为什么要禁止在某些内置函数之外使用某些机器指令?让编译器自由优化! - Basile Starynkevitch
1
@BasileStarynkevitch 这将允许编译器在其他位置也发出SSE2/3/3S/4.1等 - 例如从自动向量化。 - innocenat
显示剩余5条评论
2个回答

10

这里有一种使用gcc的方法可能是可以接受的。所有源代码都放在一个单独的源文件中。单个源文件被分成几个部分。其中一个部分根据使用的命令行选项生成代码。像main()和处理器特性检测之类的函数放在此部分中。另一个部分根据目标覆盖编译指示生成代码。可以使用目标覆盖值支持的内置函数。只有在处理器特性检测确认所需的处理器特性存在后,才应调用此部分中的函数。此示例仅具有一个AVX2代码的覆盖部分。编写针对多个目标进行优化的函数时,可以使用多个覆盖部分。

// temporarily switch target so that all x64 intrinsic functions will be available
#pragma GCC push_options
#pragma GCC target ("arch=core-avx2")
#include <intrin.h>
// restore the target selection
#pragma GCC pop_options

//----------------------------------------------------------------------------
// the following functions will be compiled using default code generation
//----------------------------------------------------------------------------

int dummy1 (int a) {return a;}

//----------------------------------------------------------------------------
// the following functions will be compiled using core-avx2 code generation
// all x64 intrinc functions are available
#pragma GCC push_options
#pragma GCC target ("arch=core-avx2")
//----------------------------------------------------------------------------

static __m256i bitShiftLeft256ymm (__m256i *data, int count)
   {
   __m256i innerCarry, carryOut, rotate;

   innerCarry = _mm256_srli_epi64 (*data, 64 - count);                        // carry outs in bit 0 of each qword
   rotate     = _mm256_permute4x64_epi64 (innerCarry, 0x93);                  // rotate ymm left 64 bits
   innerCarry = _mm256_blend_epi32 (_mm256_setzero_si256 (), rotate, 0xFC);   // clear lower qword
   *data    = _mm256_slli_epi64 (*data, count);                               // shift all qwords left
   *data    = _mm256_or_si256 (*data, innerCarry);                            // propagate carrys from low qwords
   carryOut   = _mm256_xor_si256 (innerCarry, rotate);                        // clear all except lower qword
   return carryOut;
   }

//----------------------------------------------------------------------------
// the following functions will be compiled using default code generation
#pragma GCC pop_options
//----------------------------------------------------------------------------

int main (void)
    {
    return 0;
    }

//----------------------------------------------------------------------------

我回家后会尝试这个。不过有些快速问题:这也适用于clang吗?需要哪个版本的GCC?非常感谢你。 - innocenat
你好,@Nat,我不知道这个方法是否适用于clang。对于gcc而言,它支持版本最早可以追溯到4.6.3,可能还有更早的版本(4.6.3是我手头可用于测试的最老版本)。 - user1940376
我已经尝试过,这个方法非常有效。不过如果有人知道clang的解决方案,我仍然很感兴趣。 - innocenat
哇,这真是个令人头疼的问题。它看起来像是一个很明显的用例。将最小 ISA 设置为 SSE2(可以在任何地方使用而不需要内部函数),然后允许任何内部函数,这样你就可以在运行时创建 SSE4.1 代码路径(例如)。 - PatrickB
1
@ScottD 我无法在 https://gcc.godbolt.org/ 上使这个工作。这个示例完整吗? - Bruno Martinez

-1

除了编译器本身的开关之外,没有办法控制编译器使用的指令集。换句话说,没有任何编译指示或其他功能,只有整体编译器标志。

这意味着实现您想要的唯一可行解决方案是使用 -msseX 并将源代码拆分成多个文件(当然,您始终可以使用各种巧妙的 #include 等方法将一个单独的文本文件作为主源,并在多个位置包含相同的文件)

当然,编译器的源代码是可用的。我相信 GCC 和 Clang/LLVM 的维护者会很乐意接受改进此问题的补丁。但请记住,“解析源代码”到“发出指令”的路径非常漫长和复杂。如果我们这样做会发生什么:

#pragma use_sse=1
void func()
{
   ... some code goes here ... 
}

#pragma use_sse=3
void func2()
{
  ...
  func();
  ...
}

现在,func函数足够短,可以内联,编译器应该内联它吗?如果是这样,对于func(),它应该使用sse1还是sse3指令。

我知道你可能不关心这种困难,但是Clang和GCC的维护者确实需要以某种方式处理这个问题。

编辑: 在声明SSE内部函数(以及许多其他内部函数)的头文件中,典型的函数看起来像这样:

extern __inline __m128 __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_add_ss (__m128 __A, __m128 __B)
{
  return (__m128) __builtin_ia32_addss ((__v4sf)__A, (__v4sf)__B);
}

builtin_ia32_addss只有在启用-msse选项时编译器才可用。因此,如果您说服编译器在-mno-sse时仍允许您使用_mm_add_ss(),它将为“__builtin_ia32_addss未在此范围内声明”(我刚试过)给出错误。

更改这种特定行为可能不是很难-代码可能只有几个地方进行“引入内置函数”。但是,我并不确定在编译器实际发出指令时是否存在其他问题。

我已经在基于Clang的编译器中使用了“内置函数”,不幸的是,在从“解析器”到“代码生成”的过程中涉及了几个步骤,其中内置函数得到了涉及。

编辑2:

与GCC相比,对于Clang来说,解决这个问题甚至更加复杂,因为编译器本身理解SSE指令,所以它只需在头文件中包含此内容:

static __inline__ __m128 __attribute__((__always_inline__, __nodebug__))
_mm_add_ps(__m128 __a, __m128 __b)
{
  return __a + __b;
}

编译器将知道如何添加一对__m128,以生成正确的SSE指令。我刚刚下载了Clang(我在家里,我的Clang工作在公司,与SSE无关,只是通用内置函数 - 我并没有真正做过太多Clang的更改,但足以大致了解内置函数的工作原理)。
然而,从您的角度来看,它不是内置函数使情况变得更糟,因为operator+的转换更加复杂。我相信编译器只会将其转换为“将这两个东西相加”,然后将其传递给LLVM进行进一步处理 - LLVM将是理解SSE指令等部分的组成部分。但对于您的目的而言,这使情况变得更糟,因为这是一个“内部函数”的事实现在几乎已经丢失了,编译器处理它的方式就像您编写了a + b一样,具有a和b长度为128位的类型副作用。这使得生成“正确的指令”并保持“所有其他”指令处于不同的SSE级别变得更加复杂。

令我惊讶的是,MSVC和英特尔编译器完美地接受了这种用法,甚至不需要#pragma。 - innocenat
好的,那就用它们吧... 我有一种感觉,实际上情况恰恰相反,它们不太支持自动向量化,因此不会在主代码中自动生成SSE指令。 - Mats Petersson
问题在于我想将使用MSVC编写的Windows代码移植到Linux上。而且我已经指定了/arch:IA-32。 - innocenat
所以,仅仅因为你喜欢某种特定的方式并不能改变现实的本质。微软通过他们的团队制作编译器,英特尔有自己的编译器团队,Clang和GCC是开源项目,这意味着任何人都可以做出贡献,但它主要由少数贡献者推动,他们可能会将其作为副业与主要工作一起进行,或者在大学进行研究工作,或者作为公司的一部分,“我们作为一家公司希望你在这个编译器上工作,因为如果编译器能够做到这一点,它有助于我们销售芯片”。所以,如果你不喜欢它,就去找那些制造它的人谈谈。 - Mats Petersson
请参见编辑内容,了解在gcc中如何实现此功能。 - Mats Petersson
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接