我应该使用 SIMD 或向量扩展还是其他什么?

2024-04-09

我目前正在开发一个开源 3D 应用程序框架c++ /questions/tagged/c%2b%2b (with c++11 /questions/tagged/c%2b%2b11)。我自己的数学库是这样设计的XNA数学库 http://msdn.microsoft.com/en-us/library/windows/desktop/ee415574%28v=vs.85%29.aspx,还与SIMD http://en.wikipedia.org/wiki/SIMD心里。但目前它并不是很快,并且存在内存对齐问题,但更多信息请参见另一个问题。

前几天我问自己为什么要自己写SSE http://de.wikipedia.org/wiki/Streaming_SIMD_Extensions代码。当优化开启时,编译器还能够生成高度优化的代码。我还可以使用“向量延伸 http://gcc.gnu.org/onlinedocs/gcc/Vector-Extensions.html" of GCC http://gcc.gnu.org/。但这一切并不是真正便携的。

我知道当我使用自己的 SSE 代码时我有更多的控制权,但这种控制通常是不必要的。

SSE的一大问题是动态内存的使用,在内存池和面向数据的设计的帮助下,动态内存的使用尽可能受到限制。

现在回答我的问题:

  • 我应该使用裸 SSE 吗?也许是封装的。

    __m128 v1 = _mm_set_ps(0.5f, 2, 4, 0.25f);
    __m128 v2 = _mm_set_ps(2, 0.5f, 0.25f, 4);
    
    __m128 res = _mm_mul_ps(v1, v2);
    
  • 或者编译器应该做这些肮脏的工作吗?

    float v1 = {0.5f, 2, 4, 0.25f};
    float v2 = {2, 0.5f, 0.25f, 4};
    
    float res[4];
    res[0] = v1[0]*v2[0];
    res[1] = v1[1]*v2[1];
    res[2] = v1[2]*v2[2];
    res[3] = v1[3]*v2[3];
    
  • 或者我应该使用 SIMD 和附加代码吗?就像具有SIMD操作的动态容器类一样,需要额外的load and store指示。

    Pear3D::Vector4f* v1 = new Pear3D::Vector4f(0.5f, 2, 4, 0.25f);
    Pear3D::Vector4f* v2 = new Pear3D::Vector4f(2, 0.5f, 0.25f, 4);
    
    Pear3D::Vector4f res = Pear3D::Vector::multiplyElements(*v1, *v2);
    

    上面的例子使用了一个虚构的类和usesfloat[4]内部及用途store and load在每个方法中,例如multiplyElements(...)。该方法使用 SSE 内部方法。

我不想使用其他库,因为我想了解更多有关 SIMD 和大规模软件设计的知识。但欢迎使用库示例。

PS:这不是一个真正的问题,而是一个设计问题。


好吧,如果您想使用 SIMD 扩展,一个好的方法是使用 SSE 内在函数(当然,请务必远离内联汇编,但幸运的是您没有将其列为替代方案)。但为了简洁起见,您应该将它们封装在一个带有重载运算符的漂亮向量类中:

struct aligned_storage
{
    //overload new and delete for 16-byte alignment
};

class vec4 : public aligned_storage
{
public:
    vec4(float x, float y, float z, float w)
    {
         data_[0] = x; ... data_[3] = w; //don't use _mm_set_ps, it will do the same, followed by a _mm_load_ps, which is unneccessary
    }
    vec4(float *data)
    {
         data_[0] = data[0]; ... data_[3] = data[3]; //don't use _mm_loadu_ps, unaligned just doesn't pay
    }
    vec4(const vec4 &rhs)
        : xmm_(rhs.xmm_)
    {
    }
    ...
    vec4& operator*=(const vec4 v)
    {
         xmm_ = _mm_mul_ps(xmm_, v.xmm_);
         return *this;
    }
    ...

private:
    union
    {
        __m128 xmm_;
        float data_[4];
    };
};

现在好的事情是,由于匿名联合(UB,我知道,但请向我展示一个 SSE 平台,在该平台上这不起作用),您可以在必要时使用标准浮点数组(例如operator[]或初始化(不要使用_mm_set_ps)) 并且仅在适当时使用 SSE。使用现代的内联编译器,封装可能不需要任何成本(我很惊讶 VC10 对这个向量类的大量计算优化了 SSE 指令,不用担心不必要的移动到临时内存变量,因为 VC8 似乎甚至喜欢无封装)。

唯一的缺点是,您需要注意正确的对齐,因为未对齐的向量不会给您带来任何好处,甚至可能比非 SSE 慢。但幸运的是,对齐要求__m128将传播到vec4(以及任何周围的类),您只需要处理动态分配,C++ 有很好的方法。你只需要创建一个基类operator new and operator delete函数(当然是所有类型的)都被正确重载,并且您的向量类将从中派生。要将您的类型与标准容器一起使用,您当然还需要专门化std::allocator(有可能std::get_temporary_buffer and std::return_temporary_buffer为了完整起见),因为它将使用全局operator new否则。

但真正的缺点是,您还需要关心以您的 SSE 向量作为成员的任何类的动态分配,这可能很乏味,但可以通过从这些类派生来再次自动化一点aligned_storage并把整个std::allocator专业化混乱成一个方便的宏。

JamesWy​​nn 有一个观点,即这些操作通常集中在一些特殊的重型计算块中(例如纹理过滤或顶点变换),但另一方面,使用这些 SSE 向量封装不会比标准引入任何开销float[4]-向量类的实现。无论如何,您都需要将这些值从内存中获取到寄存器中(无论是 x87 堆栈还是标量 SSE 寄存器)才能进行任何计算,所以为什么不一次将它们全部取出(恕我直言,这应该不会比移动单个值慢)值(如果正确对齐)并并行计算。因此,您可以自由地将 SSE 实现切换为非 SSE 实现,而不会产生任何开销(如果我的推理错误,请纠正我)。

但是如果确保所有类别的对齐vec4因为成员对你来说太乏味了(恕我直言,这是这种方法的唯一缺点),你还可以定义一个专门的 SSE 向量类型,用于计算并使用标准的非 SSE 向量进行存储。


EDIT:好吧,看看这里的开销参数(乍一看很合理),让我们进行一堆计算,由于重载的运算符,这些计算看起来非常干净:

#include "vec.h"
#include <iostream>

int main(int argc, char *argv[])
{
    math::vec<float,4> u, v, w = u + v;
    u = v + dot(v, w) * w;
    v = abs(u-w);
    u = 3.0f * w + v;
    w = -w * (u+v);
    v = min(u, w) + length(u) * w;
    std::cout << v << std::endl;
    return 0;
}

看看 VC10 对此有何看法:

...
; 6   :     math::vec<float,4> u, v, w = u + v;

movaps  xmm4, XMMWORD PTR _v$[esp+32]

; 7   :     u = v + dot(v, w) * w;
; 8   :     v = abs(u-w);

movaps  xmm3, XMMWORD PTR __xmm@0
movaps  xmm1, xmm4
addps   xmm1, XMMWORD PTR _u$[esp+32]
movaps  xmm0, xmm4
mulps   xmm0, xmm1
haddps  xmm0, xmm0
haddps  xmm0, xmm0
shufps  xmm0, xmm0, 0
mulps   xmm0, xmm1
addps   xmm0, xmm4
subps   xmm0, xmm1
movaps  xmm2, xmm3

; 9   :     u = 3.0f * w + v;
; 10   :    w = -w * (u+v);

xorps   xmm3, xmm1
andnps  xmm2, xmm0
movaps  xmm0, XMMWORD PTR __xmm@1
mulps   xmm0, xmm1
addps   xmm0, xmm2

; 11   :    v = min(u, w) + length(u) * w;

movaps  xmm1, xmm0
mulps   xmm1, xmm0
haddps  xmm1, xmm1
haddps  xmm1, xmm1
sqrtss  xmm1, xmm1
addps   xmm2, xmm0
mulps   xmm3, xmm2
shufps  xmm1, xmm1, 0

; 12   :    std::cout << v << std::endl;

mov edi, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A
mulps   xmm1, xmm3
minps   xmm0, xmm3
addps   xmm1, xmm0
movaps  XMMWORD PTR _v$[esp+32], xmm1
...

即使没有彻底分析每条指令及其使用,我也非常有信心地说,除了开头的那些(好吧,我让它们未初始化)之外,没有任何不必要的加载或存储,无论如何,这些加载或存储都是必要的将它们从内存放入计算寄存器,最后,这是必要的,如以下表达式所示v将会被赶出去。它甚至没有将任何东西存储回u and w,因为它们只是临时变量,我不再使用它们。一切都完美内联并优化。它甚至成功地无缝地对接下来的乘法的点积结果进行洗牌,而无需离开 XMM 寄存器,尽管dot函数返回一个float使用实际的_mm_store_ss之后haddpss.

因此,即使是我,通常对编译器的能力有点过度怀疑,也不得不说,与通过封装获得的干净且富有表现力的代码相比,将自己的内在函数手工制作成特殊函数并不真正值得。尽管您也许能够创建杀手级示例,其中手工编写的内在函数确实可以为您节省一些指令,但话又说回来,您首先必须比优化器更聪明。


EDIT:好的,Ben Voigt 指出了联合体的另一个问题,除了(很可能没有问题)内存布局不兼容之外,它违反了严格的别名规则,并且编译器可能会优化访问不同联合体成员的指令,从而使代码无效。我还没想过。我不知道它在实践中是否会产生任何问题,当然需要调查。

如果这确实是一个问题,不幸的是我们需要放弃data_[4]会员并使用__m128独自的。对于初始化,我们现在必须求助于_mm_set_ps and _mm_loadu_ps再次。这operator[]变得有点复杂,可能需要一些组合_mm_shuffle_ps and _mm_store_ss。但对于非常量版本,您必须使用某种代理对象将分配委托给相应的 SSE 指令。必须研究编译器在特定情况下可以以何种方式优化这种额外的开销。

或者,您只使用 SSE 向量进行计算,并创建一个接口用于与非 SSE 向量进行整体转换,然后在计算的外围设备中使用该接口(因为您通常不需要访问内部的各个组件)冗长的计算)。这似乎就是这样glm处理这个问题。但我不知道如何Eigen处理它。

但是无论您如何解决它,如果不利用运算符重载的好处,仍然不需要手工制作 SSE 本质。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

我应该使用 SIMD 或向量扩展还是其他什么? 的相关文章

  • 将 Stream 反序列化为 List 或任何其他类型

    尝试将流反序列化为List
  • 在动态事件处理程序中引用“this”

    在我的 myClass 类中 我使用 Reflection Emit 为 myClass 类成员之一动态编写事件处理程序 我已经成功地做到了这一点 现在 我想修改事件处理程序以调用 myClass 类中的实例方法之一 但是 我无法弄清楚如何
  • 如何将包含 5000 条记录的 Excel 文件插入到 documentDB 中?

    我有一个 Excel 文件 最初约有 200 行 我能够将 Excel 文件转换为数据表 并且所有内容都正确插入到 documentdb 中 Excel 文件现在有 5000 行 在插入 30 40 条记录后不会插入 其余所有行不会插入到
  • 如何从RichTextBox中获取显示的文本?

    如何获得显示的RichTextBox 中的文本 我的意思是 如果 RichTextBox 滚动到末尾 我只想接收那些对我来说可见的行 P S 获得第一个显示的字符串就足够了 您想使用 RichTextBox GetCharIndexFrom
  • 为什么 VB.NET 和 C# 中针对值检查 null 存在差异?

    In VB NET http en wikipedia org wiki Visual Basic NET有时候是这样的 Dim x As System Nullable Of Decimal Nothing Dim y As System
  • 将图像文件从网址复制到本地文件夹?

    我有该图像的网址 例如 http testsite com web abc jpg http testsite com web abc jpg 我想将该 URL 复制到 c images 中的本地文件夹中 而且当我将该文件复制到文件夹中时
  • 获取 std::variant 当前持有的 typeid(如 boost::variant type())

    我已经从 boost variant 迁移到 std variant 但遇到了障碍 我在 boost type 中使用了一个很好的函数 它可以让你获取当前持有的 typeid 看https www boost org doc libs 1
  • 键盘加速器在 UWP 应用中停止工作

    我正在尝试将键盘加速器添加到 UWP 应用程序中的 CommandBar 菜单项 当应用程序启动时 这工作正常 但在我第一次打开溢出菜单后 加速器停止工作 这似乎不会发生在主要命令 菜单之外 上 只有溢出菜单内的辅助命令才会发生 此外 单击
  • 找到的程序集的清单定义与程序集引用不匹配

    我试图在 C Windows 窗体应用程序 Visual Studio 2005 中运行一些单元测试 但出现以下错误 System IO FileLoadException 无法加载文件或程序集 实用程序 版本 1 2 0 200 文化 中
  • 有没有办法使用 i387 fsqrt 指令获得正确的舍入?

    有没有办法使用 i387 fsqrt 指令获得正确的舍入 除了改变精确模式在 x87 控制字中 我知道这是可能的 但这不是一个合理的解决方案 因为它存在令人讨厌的重入型问题 如果 sqrt 操作中断 精度模式将出错 我正在处理的问题如下 x
  • 如何使用 wpf webbrowser 将数据发布到 Web 服务器

    我想从数据库获取数据并使用它来让用户登录到网站 我有一个包含 Web 浏览器控件的 wpf 页面 我有这样的代码 用于将用户登录到用 php 编写的网站
  • 默认值 C# 类 [重复]

    这个问题在这里已经有答案了 我在控制器中有一个函数 并且我收到表单的信息 我有这个代码 public Actionresult functionOne string a string b string c foo 我尝试将其转换为类似的类
  • C# datagridview 列转入数组

    我正在用 C 构建一个程序 并在其中包含一个 datagridview 组件 datagridview 有固定数量的列 2 我想将其保存到两个单独的数组中 但行数确实发生了变化 我怎么能这样做呢 假设一个名为 dataGridView1 的
  • 操纵 setter 以避免 null

    通常我们有 public string code get set 如果最终有人将代码设置为 null 我需要避免空引用异常 我尝试这个想法 有什么帮助吗 public string code get set if code null cod
  • C#中Enum中定义的value__是什么

    What value 可能在这里 value MSN ICQ YahooChat GoogleTalk 我运行的代码很简单 namespace EnumReflection enum Messengers MSN ICQ YahooChat
  • 允许使用什么类型的内容作为 C 预处理器宏的参数?

    老实说 我很了解 C 编程语言的语法 但对 C 预处理器的语法几乎一无所知 尽管我有时在编程实践中使用它 所以问题来了 假设我们有一个简单的宏 它扩展为空 define macro param 可以放入宏调用构造中的语法有哪些限制 调用宏时
  • 设计 Javascript 前端 <-> C++ 后端通信

    在我最近的将来 我将不得不制作一个具有 C 后端和 Web 前端的系统 要求 目前 我对此了解不多 我认为前端将触发数据传输 而不是后端 所以不需要类似 Comet 的东西 由于在该领域的经验可能很少 我非常感谢您对我所做的设计决策的评论
  • 在 Linux 上将 libquadmath 与 C++ 链接

    我有一个示例代码 include
  • 无法使 Polly 超时策略覆盖 HttpClient 默认超时

    我正在使用 Polly 重试策略 并且正如预期的那样 在重试过程中HttpClient达到 100 秒超时 我尝试了几种不同的方法来合并 Polly 超时策略 将超时移至每次重试而不是总计 但 100 秒超时仍然会触发 我读过大约 5 个
  • FindAsync 很慢,但是延迟加载很快

    在我的代码中 我曾经使用加载相关实体await FindAsync 希望我能更好地遵守 C 异步指南 var activeTemplate await exec DbContext FormTemplates FindAsync exec

随机推荐