C# 和 SIMD:高加速和低加速。怎么了?

2024-01-03

问题介绍

我正在尝试加快我正在编写的(2d)光线追踪器的相交代码。我使用 C# 和 System.Numerics 库来提高 SIMD 指令的速度。

问题是我得到了奇怪的结果,有超顶加速和相当低的加速。我的问题是,为什么一个是在屋顶之上,而另一个是相当低的?

Context:

  • RayPack 结构是一系列(不同的)光线,包装在 System.Numerics 的向量中。
  • BoundingBoxPack 和 CirclePack 结构是单个 bb / 圆,包装在 System.Numerics 向量中。
  • 使用的 CPU 是 i7-4710HQ (Haswell),具有 SSE 4.2、AVX(2) 和 FMA(3) 指令。
  • 在发布模式下运行(64 位)。该项目在 .Net Framework 472 中运行。未设置其他选项。

Attempts

我已经尝试查找某些操作是否可能得到正确支持(请注意:这些操作适用于 c++)。https://fgiesen.wordpress.com/2016/04/03/sse-mind-the-gap/ https://fgiesen.wordpress.com/2016/04/03/sse-mind-the-gap/ or http://sci.tuomastonteri.fi/programming/sse http://sci.tuomastonteri.fi/programming/sse),但情况似乎并非如此,因为我使用的笔记本电脑支持 SSE 4.2。

在当前代码中,应用了以下更改:

  • 使用更正确的指令(例如,包装分钟)。
  • 没有使用float * vector指令(会导致很多额外的操作,参见原文的汇编)。

代码...片段?

对于大量的代码表示歉意,但我不确定在没有这么多代码的情况下我们如何具体讨论这个问题。

Ray 代码 -> BoundingBox

public bool CollidesWith(Ray ray, out float t)
{
    // https://gamedev.stackexchange.com/questions/18436/most-efficient-aabb-vs-ray-collision-algorithms
    // r.dir is unit direction vector of ray
    float dirfracx = 1.0f / ray.direction.X;
    float dirfracy = 1.0f / ray.direction.Y;
    // lb is the corner of AABB with minimal coordinates - left bottom, rt is maximal corner
    // r.org is origin of ray
    float t1 = (this.rx.min - ray.origin.X) * dirfracx;
    float t2 = (this.rx.max - ray.origin.X) * dirfracx;
    float t3 = (this.ry.min - ray.origin.Y) * dirfracy;
    float t4 = (this.ry.max - ray.origin.Y) * dirfracy;

    float tmin = Math.Max(Math.Min(t1, t2), Math.Min(t3, t4));
    float tmax = Math.Min(Math.Max(t1, t2), Math.Max(t3, t4));

    // if tmax < 0, ray (line) is intersecting AABB, but the whole AABB is behind us
    if (tmax < 0)
    {
        t = tmax;
        return false;
    }

    // if tmin > tmax, ray doesn't intersect AABB
    if (tmin > tmax)
    {
        t = tmax;
        return false;
    }

    t = tmin;
    return true;
}

RayPak 代码 -> Bounding Box Pack

public Vector<int> CollidesWith(ref RayPack ray, out Vector<float> t)
{
    // ------------------------------------------------------- \\
    // compute the collision.                                  \\

    Vector<float> dirfracx = Constants.ones / ray.direction.x;
    Vector<float> dirfracy = Constants.ones / ray.direction.y;

    Vector<float> t1 = (this.rx.min - ray.origin.x) * dirfracx;
    Vector<float> t2 = (this.rx.max - ray.origin.x) * dirfracx;
    Vector<float> t3 = (this.ry.min - ray.origin.y) * dirfracy;
    Vector<float> t4 = (this.ry.max - ray.origin.y) * dirfracy;

    Vector<float> tmin = Vector.Max(Vector.Min(t1, t2), Vector.Min(t3, t4));
    Vector<float> tmax = Vector.Min(Vector.Max(t1, t2), Vector.Max(t3, t4));

    Vector<int> lessThanZeroMask = Vector.GreaterThan(tmax, Constants.zeros);
    Vector<int> greaterMask = Vector.LessThan(tmin, tmax);
    Vector<int> combinedMask = Vector.BitwiseOr(lessThanZeroMask, greaterMask);

    // ------------------------------------------------------- \\
    // Keep track of the t's that collided.                    \\

    t = Vector.ConditionalSelect(combinedMask, tmin, Constants.floatMax);

    return combinedMask;
}

射线代码 -> 圆

public bool Intersect(Circle other)
{
    // Step 0: Work everything out on paper!

    // Step 1: Gather all the relevant data.
    float ox = this.origin.X;
    float dx = this.direction.X;

    float oy = this.origin.Y;
    float dy = this.direction.Y;

    float x0 = other.origin.X;
    float y0 = other.origin.Y;
    float cr = other.radius;

    // Step 2: compute the substitutions.
    float p = ox - x0;
    float q = oy - y0;

    float r = 2 * p * dx;
    float s = 2 * q * dy;

    // Step 3: compute the substitutions, check if there is a collision.
    float a = dx * dx + dy * dy;
    float b = r + s;
    float c = p * p + q * q - cr * cr;

    float DSqrt = b * b - 4 * a * c;

    // no collision possible! Commented out to make the benchmark more fair
    //if (DSqrt < 0)
    //{ return false; }

    // Step 4: compute the substitutions.
    float D = (float)Math.Sqrt(DSqrt);

    float t0 = (-b + D) / (2 * a);
    float t1 = (-b - D) / (2 * a);

    float ti = Math.Min(t0, t1);
    if(ti > 0 && ti < t)
    {
        t = ti;
        return true;
    }

    return false;
}

RayPack -> CirclePack 代码 原始的、未经编辑的代码可以在以下位置找到:https://pastebin.com/87nYgZrv https://pastebin.com/87nYgZrv

public Vector<int> Intersect(CirclePack other)
{
    // ------------------------------------------------------- \\
    // Put all the data on the stack.                          \\

    Vector<float> zeros = Constants.zeros;
    Vector<float> twos = Constants.twos;
    Vector<float> fours = Constants.fours;

    // ------------------------------------------------------- \\
    // Compute whether the ray collides with the circle. This  \\
    // is stored in the 'mask' vector.                         \\

    Vector<float> p = this.origin.x - other.origin.x; ;
    Vector<float> q = this.origin.y - other.origin.y;

    Vector<float> r = twos * p * this.direction.x;
    Vector<float> s = twos * q * this.direction.y; ;

    Vector<float> a = this.direction.x * this.direction.x + this.direction.y * this.direction.y;
    Vector<float> b = r + s;
    Vector<float> c = p * p + q * q - other.radius * other.radius;

    Vector<float> DSqrt = b * b - fours * a * c;

    Vector<int> maskCollision = Vector.GreaterThan(DSqrt, zeros);

    // Commented out to make the benchmark more fair.
    //if (Vector.Dot(maskCollision, maskCollision) == 0)
    //{ return maskCollision; }

    // ------------------------------------------------------- \\
    // Update t if and only if there is a collision. Take      \\
    // note of the conditional where we compute t.             \\

    Vector<float> D = Vector.SquareRoot(DSqrt);

    Vector<float> bMinus = Vector.Negate(b);
    Vector<float> twoA = twos * a;
    Vector<float> t0 = (bMinus + D) / twoA;
    Vector<float> t1 = (bMinus - D) / twoA;
    Vector<float> tm = Vector.ConditionalSelect(Vector.LessThan(t1, t0), t1, t0);

    Vector<int> maskBiggerThanZero = Vector.GreaterThan(tm, zeros);
    Vector<int> maskSmallerThanT = Vector.LessThan(tm, this.t);

    Vector<int> mask = Vector.BitwiseAnd(
        maskCollision,
        Vector.BitwiseAnd(
            maskBiggerThanZero,
            maskSmallerThanT)
            );

    this.t = Vector.ConditionalSelect(
        mask,                                                           // the bit mask that allows us to choose.
        tm,                                                             // the smallest of the t's.
        t);                                                             // if the bit mask is false (0), then we get our original t.

    return mask;
}

汇编代码

这些可以在pastebin上找到。请注意,基准测试工具中有一些样板组件。您需要查看函数调用。

  • 边界框(包):https://pastebin.com/RYMQdZMh https://pastebin.com/RYMQdZMh
  • 圆(包)调整:https://pastebin.com/YZHjc1vY https://pastebin.com/YZHjc1vY
  • 圆(包)原图:https://pastebin.com/87nYgZrv https://pastebin.com/87nYgZrv

标杆管理

我一直在使用 BenchmarkDotNet 对情况进行基准测试。

Circle / CirclePack 的结果(更新):

Method Mean Error StdDev
Intersection 9.710 ms 0.0540 ms 0.0505 ms
IntersectionPacked 3.296 ms 0.0055 ms 0.0051 ms

边界框/边界框填充的结果:

Method Mean Error StdDev
Intersection 24.269 ms 0.2663 ms 0.2491 ms
IntersectionPacked 1.152 ms 0.0229 ms 0.0264 ms

由于 AVX,预计加速大约为 6 倍到 8 倍。边界框的加速比为重要的,而圆的加速比相当低。

重新审视顶部的问题:为什么一个加速比非常高,而另一个则相当低?两者中较低的一个(CirclePack)如何变得更快?

关于 Peter Cordes 的编辑(评论)

  • 使基准测试更加公平:一旦光线不再发生碰撞,单光线版本就不会提前分支。现在加速大约是 2.5 倍。
  • 添加汇编代码作为单独的标头。
  • 关于平方根:这确实有影响,但没有看起来那么大。去除向量平方根可将总时间减少约 0.3ms。单射线代码现在也始终执行平方根。
  • 关于 C# 中的 FMA(融合乘加)的问题。我认为它适用于标量(参见C# 可以使用乘加融合吗? https://stackoverflow.com/questions/37443569/can-c-sharp-make-use-of-fused-multiply-add),但我没有在 System.Numerics.Vector 结构中找到类似的操作。
  • 关于压缩 min 的 C# 指令:是的,确实如此。愚蠢的我。我什至已经用过它了。

我不会尝试回答有关 SIMD 加速的问题,而是提供一些关于标量版本中的不良编码的详细评论,这些编码会延续到矢量版本,以一种不适合 SO 评论的方式。

这段代码在Intersect(Circle)简直是荒谬的:

// Step 3: compute the substitutions, check if there is a collision.
float a = dx * dx + dy * dy;

平方和 ->a保证非负

float b = r + s;
float c = p * p + q * q - cr * cr;

float DSqrt = b * b - 4 * a * c;

这不是 D 的平方根,而是 D 的平方。

// no collision possible! Commented out to make the benchmark more fair
//if (DSqrt < 0)
//{ return false; }

// Step 4: compute the substitutions.
float D = (float)Math.Sqrt(DSqrt);

sqrt具有有限的域。避免对负输入的调用不仅可以节省平方根的平均成本,还可以使您不必处理非常非常慢的 NaN。

Also, D是非负的,因为Math.Sqrt返回正分支或 NaN。

float t0 = (-b + D) / (2 * a);
float t1 = (-b - D) / (2 * a);

这两者之间的区别是t0 - t1 = D / a。两个非负变量的比率也是非负的。所以t1永远不会更大。

float ti = Math.Min(t0, t1);

这个电话always选择t1。计算t0更大的测试是一种浪费。

if(ti > 0 && ti < t)
{
    t = ti;
    return true;
}

回想起来ti总是t1, and a是非负的,第一个测试相当于-b - D > 0 or b < -D.


在SIMD版本中,Vector.SquareRoot文档没有描述输入为负时的行为。和Vector.LessThan没有描述输入为 NaN 时的行为。

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

C# 和 SIMD:高加速和低加速。怎么了? 的相关文章

随机推荐

  • Chrome 切换到 OSX 通知后,网络推送通知未显示

    由于 Chrome 切换到本机 OSX 通知 有时我尝试使用网络推送和我的 Service Worker 显示的通知不会出现 它们在 Chrome 的早期版本中一致出现 自从他们开始做这项工作以来 我应该改变什么吗 确保 请勿打扰 已关闭
  • 快速字符串替换

    在构建了一个可能非常大的字符串后 我将对其进行大量更改 将其中的单个字符 或字节 如果需要 更改为另一个字符 实际上 我的脚本正在构建一个填字游戏 因此字符串不会很长 但我的问题很笼统 我如何利用我不改变字符串 或任何更好的数据类型 长度的
  • 如何增加 UIButton 的点击区域?

    I use UIButton具有自动布局 当图像较小时 点击区域也较小 我可以想象几种方法来解决这个问题 增加图像尺寸 即在图像周围放置透明区域 这不好 因为当您定位图像时 您必须记住额外的透明边框 使用 CGRectInset 并增加大小
  • 在 Solaris 上构建 Node.JS:“使用 仅在 c99 编译环境中有效。”

    我正在尝试在 Solaris 上安装 Node JS 这是开箱即用的 Solaris 9 10 x86 最新版本是 2010 年 9 月 并且仅使用默认软件包 我遵循的方向在这里 https github com joyent node w
  • 在Android 4.0.3中设置textview淡入淡出

    我刚刚尝试实现淡入淡出效果TextView在安卓4 0 3中 然而 它不起作用 fadingEdge horizontal singleLine true ellipsize marquee 此代码适用于 2 3 7 及以下版本 但不适用于
  • Dart 作为 Worker 进行隔离

    编辑以使问题更清楚 我正在尝试在 Dart 中使用 Isolates 或 Web Workers 我能找到在主线程和隔离线程之间进行通信的唯一方法是send and 打电话然后从主线程 但这是主线程将一些数据传递给隔离的好方法 如果我希望分
  • GitHub 推送错误 - “git 媒体更新”

    我一直在使用 GitHub for Mac 客户端 两个月来它一直工作得很好 昨天 成功提交和推送后 我的客户端自行关闭然后重新启动 从那时起 我就无法将更改推送到我的在线存储库 在客户端中 我收到消息 git media pre push
  • 在Sikuli拍照的命令是什么

    我正在使用 Sikuli IDE 我想知道截屏的命令是什么 这样我就可以在测试结束时捕获屏幕 像这样的东西 try if bla bla bla print blablabla else TAKESCREENSHOT gt What com
  • 在 stripes actionbean 中绑定隐藏字段时,隐藏字段变为 null

    我有一个条纹操作页面 当页面加载时 我想通过从对象 即 setOriginalAssignee userAction getAssignee 分配原始Assignee 来保存它 这样 如果对象的字段受让人发生更改 我将进行一些计算 这是我的
  • 计算根的父母拥有的百分比

    简而言之 我试图计算树的父级所拥有的树根的百分比 即树的更上层 我怎样才能单独用 SQL 来做到这一点 这是我的 示例 架构 请注意 虽然层次结构本身非常简单 但还有一个附加的holding id 这意味着单亲父母可以 拥有 孩子的不同部分
  • 如何使用 SymPy codegen 生成 Fortran 子例程

    我想使用 SymPy codegen 实用程序生成 Fortran 子例程 我可以毫无问题地生成 Fortran 函数codegen f x y z f95 filename 但我想生成一个 Fortran 子例程 以便可以修改输入数组 我
  • 将 ZedGraph 中的 DateAsOrdinal xAxis 标签格式化

    我现在已经改变了我的x axis to 日期为序号 但我想改进标签格式 我目前处理的是XAxis ScaleFormatEvent像这样 Private Function OnXScaleFormatEvent ByVal pane As
  • C++11 移动语义是在做一些新的事情,还是只是让语义更清晰?

    我基本上想弄清楚 整个 移动语义 概念是全新的 还是只是使现有代码更易于实现 我总是对减少调用复制 构造函数的次数感兴趣 但我通常使用引用 可能还有 const 来传递对象 并确保我始终使用初始化列表 考虑到这一点 并查看了整个丑陋的 语法
  • 向 C# 控制台应用程序添加“--help”参数

    我通过命令行使用的几乎所有 exe 都有一个由 help 命令调用的帮助函数 我如何在 C 中实现这个 难道只是检查args 中的参数是否是字符串 help 那么简单吗 使用 nix 命令 通常可以通过以下方式获取帮助 h or help
  • 如何在android中将数据库写入文本文件

    我正在为我的大学项目开发 一个间谍应用程序 为此 我记录了设备的通话 位置和短信并将它们存储在数据库中 现在我想将数据库的内容导出到文本文件 我尝试了下面的代码 private void readAndWriteCallsData File
  • 声明时没有实例化对象的原因是什么?

    最近我不得不深入研究一些 VB6 代码 我到处都看到了这种模式 dim o as obj set o new obj 为什么不是这个 dim o as new obj 我记得15年前有一个很好的理由 但现在我不记得是什么了 有人记得吗 理由
  • 检查元素是否为 div

    我如何检查是否 this is a div ul or blockquote 例如 if this is a div alert its a div else alert its not a div some other stuff 像这样
  • 蒙古人名的处理

    有几个国家 土耳其 蒙古 吉尔吉斯斯坦等 通常男性的名字可以没有中间名 而是使用 oglu uulu 等词代替 例如 Michael oglu Bret 意思是 布雷特的迈克尔儿子 我曾经把这类词分开 并把它们当作中间名 所以在过去的一周里
  • 在画布绘图上设置触摸监听器

    假设我在画布上绘制了位图图像或简单的圆圈 如何设置 OnTouchListener 来检查我的绘图是否被触摸 由于我将在画布上绘制多个圆圈 因此我希望每个圆圈都有一些唯一的 ID 以便我可以相应地工作 当您触摸屏幕时 获取 x 和 y 坐标
  • C# 和 SIMD:高加速和低加速。怎么了?

    问题介绍 我正在尝试加快我正在编写的 2d 光线追踪器的相交代码 我使用 C 和 System Numerics 库来提高 SIMD 指令的速度 问题是我得到了奇怪的结果 有超顶加速和相当低的加速 我的问题是 为什么一个是在屋顶之上 而另一