【问题标题】:How to avoid `out` parameter error when using intrinsics?使用内在函数时如何避免“out”参数错误?
【发布时间】:2020-01-26 04:27:21
【问题描述】:

我正在尝试添加到 .NET Core 3.0 中的新硬件内在函数,特别是为了加速矩阵运算。对于矩阵加法,我有一个函数,它采用两个 4x4 float 矩阵作为 in 参数,以及第三个 out 矩阵来存储结果。它使用 SSE 128 位向量内在函数来添加和存储结果输出:

public unsafe static void Add(in Matrix l, in Matrix r, out Matrix o)
{
    fixed (float* lp = &l.m00, rp = &r.m00, op = &o.m00)
    {
        var c1 = Sse.Add(Sse.LoadVector128(lp + 0),  Sse.LoadVector128(rp + 0));
        var c2 = Sse.Add(Sse.LoadVector128(lp + 4),  Sse.LoadVector128(rp + 4));
        var c3 = Sse.Add(Sse.LoadVector128(lp + 8),  Sse.LoadVector128(rp + 8));
        var c4 = Sse.Add(Sse.LoadVector128(lp + 12), Sse.LoadVector128(rp + 12));
        Sse.Store(op + 0,  c1);
        Sse.Store(op + 4,  c2);
        Sse.Store(op + 8,  c3);
        Sse.Store(op + 12, c4);
    }
}

现在显然 C# 编译器对此存在问题,因为它无法判断输出矩阵是否已写入,因此它会生成函数无法返回的错误,直到分配了 o 变量。 我的问题是是否有任何解决方法,而不必在执行内部操作之前求助于分配给变量,例如 o = default; 作为函数的第一行。

我最初是这样考虑的:

var op = stackalloc float[16];
fixed (float* lp = &l.m00, rp = &r.m00)
{
...
}
o = *(Matrix*)op;

但意识到这并不能避免复制结构,这消除了将矩阵作为out 传递的整个要点。

我意识到如果我将输出矩阵作为ref 传递,或者如果我只是从函数返回一个矩阵实例,这将起作用,但保留有用的内联语法 (Matrix.Add(l, r, out Matrix o)) 和通过引用传递大值类型可以提高性能。

【问题讨论】:

  • 简单回答;不。 C# 要求初始化所有 out 参数。
  • 我害怕,但接受,这很可能是这种情况。我很好奇人们是否有任何技巧可以解决这个特定问题。
  • 使用out 不复制结构吗? 我真的不知道答案。
  • @mjwills 对于值类型,out 参数将传递对该类型内存块的引用,该内存块可以直接写入。当函数返回时,这消除了对结构副本的需要。
  • 对于它的价值,我认为内联声明语法的好处是非常。我喜欢尽可能使用它,但如果无法访问它,甚至不会造成严重的不便。也就是说,请注意,即使您使用ref,您也需要初始化该对象。只是你必须在调用你的方法之前初始化它,而不是在它内部。您是否考虑过使用new Matrix() 对其进行初始化?我猜编译器会优化得相当好(如果不是 csc,那么 JIT 编译器)

标签: c# .net-core intrinsics out


【解决方案1】:

我在这里假设您使用的是Matrix 类型,即struct。显然,如果它是引用类型,那么您的方法实际上必须先初始化参数值才能使用它,因此您的代码并没有向我表明它是值类型。

不能使 C# 编译器忽略编译时错误。在方法返回之前未初始化out 参数是编译时错误。所以你被卡住了。

也就是说,我认为这不应该是一个重大的困难。你可以这样写你的方法:

public unsafe static void Add(in Matrix l, in Matrix r, out Matrix o)
{
    o = default(Matrix);

    fixed (float* lp = &l.m00, rp = &r.m00, op = &o.m00)
    {
        var c1 = Sse.Add(Sse.LoadVector128(lp + 0),  Sse.LoadVector128(rp + 0));
        var c2 = Sse.Add(Sse.LoadVector128(lp + 4),  Sse.LoadVector128(rp + 4));
        var c3 = Sse.Add(Sse.LoadVector128(lp + 8),  Sse.LoadVector128(rp + 8));
        var c4 = Sse.Add(Sse.LoadVector128(lp + 12), Sse.LoadVector128(rp + 12));
        Sse.Store(op + 0,  c1);
        Sse.Store(op + 4,  c2);
        Sse.Store(op + 8,  c3);
        Sse.Store(op + 12, c4);
    }
}

这将编译成类似这样的内容(为了示例,我选择了一个任意的 Matrix 类型……显然不是您使用的类型,但基本前提是相同的):

IL_0000:  ldarg.0
IL_0001:  initobj    System.Windows.Media.Matrix

这又会简单地initialize the block of memory to 0 values:

initobj 指令将由推送地址(类型为native int&*)指定的值类型的每个字段初始化为空引用或相应原始类型的0。调用此方法后,实例已准备好调用构造方法。如果typeTok 是引用类型,则该指令与ldnull 后跟stind.ref 的效果相同。

Newobj 不同,initobj 不调用构造函数方法。 Initobj 用于初始化值类型,而newobj 用于分配和初始化对象。

换句话说,initobj,也就是你在使用default(Matrix) 时得到的,是一个非常简单的初始化,只是将内存位置清零。它应该足够快,并且在任何情况下显然都比分配一个全新的对象副本然后将结果复制回原始变量(无论是在本地完成还是通过返回值完成)要少。

话虽如此,这在很大程度上取决于您将如何调用该方法的上下文。虽然您说您希望保留内联声明的便利性,但我不清楚为什么您希望对于一个显然对性能至关重要的方法可以使用 SSE 功能和不安全的代码。使用内联声明,您必须在每次调用时重新初始化变量。

如果这个方法实际上是以性能关键的方式被调用的,那么对我来说这意味着它在一个循环中被调用了很多次,可能是数百万次或更多。在这种情况下,您可能更喜欢 ref 选项,您可以在循环外初始化变量,然后为每次调用重复使用该变量,而不是为每次调用重新声明一个新变量。

【讨论】:

  • 只是将内存位置清零。它应该足够快...也许你没有意识到这个函数的实际工作是多么的便宜。如果归零在到达真正的优化器后没有优化掉,那么另外 4 个 movups 指令将占该函数总 uop 计数的很大一部分。例如4x movups 加载,4x addps 与内存源(假设对齐源或如果 AVX 可用),4x movups 存储。所以 12 个融合域微指令。将寄存器归零并再进行 4 次存储将使其达到 17 微秒。此外,存储吞吐量仅为 1/时钟(直到 Icelake)
  • AMD 和 Intel 的分析基本相同,只是 AMD 不能每个时钟执行 2 次加载 + 1 次存储。如果此函数内联到数据已经在寄存器中的调用者,则它只是 4x addps 而不必存储,例如如果 4x4 矩阵将成为其他东西的输入。顺便说一句,如果矩阵很大,写两次会更加更多昂贵。 (例如,如果商店可能在缓存中丢失,那么两次通行证都将支付该费用。)因此,只有当您可以证明虚拟 init 在实践中优化掉时,这个答案才是好的。
  • @Peter:谢谢。我的答案是从更高级别的 C# 问题的角度提出的。 OP 说他们想使用参数的内联声明,目前还不清楚他们是否真的需要这里的性能。深入挖掘计数周期的本质超出了我的预期范围。相反,专注于可用的 C# 抽象与不可用的 C# 抽象。如果您发现缺少答案,我鼓励您发布自己的答案,以便 OP 可以从您的智慧中受益。
  • 如果我有 C# 编译器并且知道该语言,我可能会检查一下。但我没有,所以我不能轻易调查。如果 OP 不需要性能,大概他们不会手动使用 SIMD。无论如何,希望你只需要让语言保持愉快,真正的编译器会尽可能优化存储/重新加载到内存,在内联后优化 CIL 到机器代码时。如果这是合理的,我会这样说答案,而不是归零的成本很小但可以忽略不计。
  • 无论如何,C# 不是有一个Vector<float> 扩展名,可以让您在没有unsafe 代码的情况下执行SIMD 吗?这可能是一个更好的主意,并在支持它们的机器上为您提供 256 位向量。除非 OP 需要这种使用 SSE 进行混洗来转置矩阵的风格? IDK 足够了解 C# 如何公开 SIMD;我只是有时会看 C# simd 问题,因为有时它们是关于 x86 可以有效地做的事情,假设有一种方法可以在 C# 中以某种方式表达它。
猜你喜欢
  • 2016-03-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-01
  • 2020-11-18
  • 1970-01-01
  • 1970-01-01
  • 2017-05-22
  • 2021-10-11
相关资源
最近更新 更多