使用内在函数时如何避免“out”参数错误?

How to avoid `out` parameter error when using intrinsics?

我正在尝试添加到 .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# 编译器对此有问题,因为它无法判断输出矩阵是否曾经被写入,所以它会生成函数无法 return 直到 [=15] 的错误=] 变量赋值给。 我的问题是是否有任何解决方法,而不必在执行内部操作之前求助于变量赋值,例如 o = default; 作为函数的第一行.

我最初考虑的是:

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

但意识到这并不能避免复制结构,这消除了将矩阵作为 out.

传递的整个要点

我意识到,如果我将输出矩阵作为 ref 传递,或者如果我只是 return 从函数中编辑一个矩阵实例,这会起作用,但最好保留有用的内联语法 (Matrix.Add(l, r, out Matrix o)) 和通过引用传递大值类型带来的性能优势。

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

无法使 C# 编译器忽略编译时错误。在方法 return 之前不初始化 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:

The initobj instruction initializes each field of the value type specified by the pushed address (of type native int, &, or *) to a null reference or a 0 of the appropriate primitive type. After this method is called, the instance is ready for a constructor method to be called. If typeTok is a reference type, this instruction has the same effect as ldnull followed by stind.ref.

Unlike Newobj, initobj does not call the constructor method. Initobj is intended for initializing value types, while newobj is used to allocate and initialize objects.

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

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

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