我希望这不会成为一个非常愚蠢的问题我以后会感到尴尬,但我总是对SIMD内在函数感到困惑,以至于我觉得它更容易比内在函数合理化汇编代码。
所以我的主要问题是使用SIMD内部数据类型,如__m256
。只是为了跳到这一点,我的问题是做这样的事情:
class PersistentObject
{
...
private:
std::vector<__m256, AlignedAlloc<__m256, 32>> data;
};
这是否可接受,在生成最有效的代码时,它会使编译器绊倒吗?这就是现在令我困惑的部分。我处于缺乏经验的水平,当我有一个热点并且已经筋疲力尽所有其他直接选择时,我会给SIMD内在函数一个镜头,如果他们不提高性能,我总是希望退出我的变化(而且我和#39;已经退出了许多与SIMD相关的更改。)
但是这个关于存储SIMD内在类型的问题和困惑仍然让我意识到我并不真正理解这些内在函数如何在基本编译器级别上工作。我的想法想要__m256
就像一个抽象的YMM
寄存器(尚未分配)。当我看到加载和存储指令时,它开始与我点击。我认为它们是编译器执行寄存器分配的提示。
我没有比以前更多考虑因为我总是以临时方式使用SIMD类型:_mm256_load_ps
到__m256
,做一些操作,存储结果返回32位SPFP 256位对齐数组float[8]
。我想到__m256
就像YMM注册一样。
摘要YMM注册?
但是最近我正在实现一个试图围绕SIMD处理的数据结构(一个简单的代表SoA方式的一组向量),如果我可以主要使用__m256
而不用从一系列花车中不断加载,然后将结果存回。在一些快速测试中,MSVC至少似乎发出了将我的内在函数映射到汇编的适当指令(以及当我从向量中访问数据时正确对齐的加载和存储)。但这打破了我对__m256
作为抽象YMM
寄存器的概念模型,因为存储这些东西会持续地暗示某些更像常规变量的东西,但在这一点上,负载是什么? / movs和stores?
所以我对我在脑海中构建的关于如何思考所有这些内容的概念模型略微嗤之以及,我的希望是,有经验的人可以立即认识到这种方式被破坏了我正在考虑这些东西并给我一个调整我大脑的eureka答案。我希望这个问题不是太愚蠢(我有一种不安的感觉,但是我试图在其他地方发现答案,但仍然发现自己很困惑)。所以最终,是否可以永久地直接存储这些数据类型(意味着我们在已经从YMM寄存器溢出而不使用_mm_load*
之后的某个时刻重新加载内存),如果是这样,我的概念模型有什么问题?
道歉,如果这是一个如此愚蠢的问题!我用这些东西真的在耳朵后面湿了。
更多细节
非常感谢你们迄今为止提供的有用评论!我想我应该分享更多细节,以使我的问题不那么模糊。基本上我试图创建一个数据结构,它只是以SoA形式存储的向量集合:
xxxxxxxx....
yyyyyyyy....
zzzzzzzz....
...主要是为了用于热点,其中关键循环具有顺序访问模式。但与此同时,非关键执行路径可能想要以AoS形式(x / y / z)随机访问第5个3向量,此时我们不可避免地进行标量访问(如果由于它们不是关键路径,所以效率不高。
在这个特例中,从实现的角度来看,我发现只是持久地存储和使用__m256
而不是float*
更方便。它会阻止我使用_mm_loads*
和_mm_stores*
散布大量垂直循环代码,因为这种情况下的常见情况(关键执行和大量代码)都是使用SIMD内在函数实现的。但我不确定这是否只是为短暂的临时数据保留__m256
,某些函数是本地的,将一些浮点数加载到__m256,执行某些操作以及存储结果就像我过去通常所做的那样。这会更加方便,但我有点担心这种方便的实现方式可能会阻碍一些优化器(尽管我还没有发现这种情况)。如果他们没有绊倒优化器,那么我一直在思考这些数据类型的方式有点过时了。
所以在这种情况下,如果完成这些事情就好了,我们的优化者总是能够很好地处理这个问题,然后我很困惑,因为我正在思考的方式这个东西,并认为我们需要在短期上下文(函数的本地,即)中明确_mm_load
和_mm_store
来帮助我们的优化器完全错误!而这种情况让我感到不安,因为我没想到它应该可以正常工作! :-D
答案
有一些来自Mysticial的评论确实对我来说很有帮助,并且帮助我修复了我的大脑,并且给了我一些保证,我想做的事情是正确的。它是以评论的形式而不是答案的形式给出的,所以我在这里引用它,以防任何人碰巧遇到类似的混淆。
如果它有帮助,我有大约200k的LOC写得完全像这样。我,我 将SIMD类型视为一等公民。没关系。编译器 处理它们与任何其他原始类型没有区别。所以那里 没问题。
优化者并不那么脆弱。他们确实在内部保持正确性 对C / C ++标准的合理解释。加载/存储 除非你需要特殊的内在函数,否则它们并不是真正需要的 (未对齐,非时间,蒙面等......)
那就是说,请随时写下你自己的答案。更多信息更好!我非常希望能够更有信心地改进对如何编写SIMD代码的基本理解,因为我处于对所有事情犹豫不决的阶段,并且仍在猜测自己。< / p>
反思
再次感谢大家!我现在感觉非常清楚,并且对设计围绕SIMD构建的代码更有信心。出于某种原因,我对SIMD内在函数的优化器非常怀疑,认为我必须以尽可能最低级别的方式编写代码,并在有限的函数范围内尽可能将这些加载和存储作为本地。我认为我的一些迷信来自于几乎几十年前编写的SIMD内在函数最初针对较旧的编译器,也许当时优化器可能需要更多的帮助,或者我可能一直都是非理性的迷信。我看起来有点像80年代人们如何看待C编译器,在这里和那里添加register
提示等内容。
使用SIMD我总是有非常喜欢的结果,并且有一种倾向,尽管每次在蓝色的月亮里一次又一次地使用它,总是觉得自己像初学者一样,也许只是因为混合成功了让我不愿意使用它,这显着延迟了我的学习过程。最近我试图纠正这一点,我非常感谢所有的帮助!
答案 0 :(得分:5)
是的,__m256
是常规类型;它不必是仅限注册的。您可以创建__m256
数组,通过引用非内联函数传递它们,以及其他任何内容。
主要警告是它是一种“过度对齐”类型:编译器假定内存中的__m256
是32字节对齐的,但std::max_align_t
通常只有8或16字节对齐主流的C ++实现。因此,您需要std::vector
或其他动态分配的自定义分配器,因为std::vector<__m256>
将分配与存储__m256
无法充分对齐的内存。谢谢,C ++(虽然C ++ 17显然最终会解决这个问题)。
但这打破了我对
__m256
作为抽象YMM寄存器的概念模型,因为存储这些东西会持续地暗示更像常规变量的东西,但那时负载/运动和存储是什么?
__m128 _mm_loadu_ps(float*)
/ _mm_load_ps
内在函数主要用于将对齐信息传递给编译器,(对于FP内在函数)传递给类型转换。使用整数你甚至不会这样做,你必须将指针转换为__m128i*
。
(AVX512内在函数最终使用void*
代替__m512i*
。)
_mm256_load_ps(fp)
基本上等同于*(__m256*)fp
:对齐加载8个浮点数。允许__m256*
为其他类型设置别名,但是(据我所知)反之亦然 not true:不能保证获得__m256 my_vec
的第3个元素是安全的像((float*)my_vec)[3]
这样的代码。这将是严格违规的违规行为。虽然它在大多数编译器中至少大部分时间都在实践中起作用。
(请参阅Get member of __m128 by index?,以及print a __m128i variable以便携方式:存储到tmp数组通常会优化掉。但如果你想要一个水平和或其他东西,它通常是best to use vector shuffle and add intrinsics,而不是希望编译器将自动向量化存储+标量添加循环。)
也许在过去的某些时候,当内在函数是新的时,每次你的C源包含movaps
时你确实得到_mm_load_ps
加载,但此时它与{*
没有特别的不同。在float*
上运行1}}运算符;编译器可以并将优化相同数据的冗余负载,或优化向量存储/标量重载到随机数。
但与此同时,非关键执行路径可能希望随机访问AoS形式的第5个3向量(x / y / z),此时我们不可避免地进行标量访问。
这里最大的警告是,从__m256
对象中获取标量的代码将变得丑陋,并且可能无法有效编译。您可以使用包装函数隐藏丑陋,但效率问题可能不会轻易消失,具体取决于您的编译器。
如果编写不使用gcc样式my_vec[3]
或MSVC my_vec.m256_f32[3]
的可移植代码,则将__m256
存储到alignas(32) float tmp [8]
之类的数组可能无法优化,你可能会加载到YMM寄存器和商店。 (然后是vzeroupper
)。