out 和 ref 可以用作临时变量吗?

Can out and ref be used as temporary variables?

当我们在计算中使用outref时,多次赋值并从中读取,它有什么缺点?会影响性能吗?

static bool TrySomeFunction(int x, int y, out int result)
{
    result = 8;
    for (int i = 0; i < x; i++)
    {
        result += result + x;
        if (result == y)
            return false;
    }
    return true;
}

或者我们最好使用额外的变量:

static bool TrySomeFunction(int x, int y, out int result)
    {
        int temp = 8;
        for (int i = 0; i < x; i++)
        {
            temp += temp + x;
            if (temp == y)
            {
                result = 0;
                return false;
            }      
        }
        result = temp;
        return true;
    }

更新:将函数名称从 SomeFunction 更改为更明确的预期用途。

事实证明,我们进行的计算越多,两者的性能差异就越大。

我相信这是意料之中的,因为我们在这里看到了额外的间接级别。 ldind and stind operations used to get/set the value for out parameter (indirectly) and ldoc with stloc 用于 get/set 局部变量的值。

我认为编译器不能在这里做任何优化(至少将 UseOutExtensively 转换为 DontUseOutExtensively),因为如果其他线程写入该位置,这可能会改变方法的行为out 个参数同时执行函数。

测试

让我稍微简化一下您的函数,以便我们只专注于我们感兴趣的内容:

void UseOutExtensively(out int result)
{
    result = 0;
    for (int i = 0; i < 100; i++)
    {
        int temp = result;
        result = temp;
    }
}

void DontUseOutExtensively(out int result)
{
    int temp = 8;
    for (int i = 0; i < 100; i++)
    {
        int anotherTemp = temp;
        temp = anotherTemp;
    }
    result = temp;
}

所以这些函数没有做任何有用的事情,它们只是在变量之间交换相同的值。因此我们没有复杂的添加和条件,只有 get/set 一个 out 变量和 get/set 一个局部变量。

所以测试程序如下:

int Iterations = 10000000; // we'll try 10^7, 10^8 && 10^9

Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < Iterations; i++)
  UseOutExtensively(out int result);

Console.WriteLine("Using out extensively: {0}",
                          sw.ElapsedMilliseconds);

sw = Stopwatch.StartNew();
for (int i = 0; i < Iterations; i++)
  DontUseOutExtensively(out int result);

Console.WriteLine("Don't use out extensively: {0}",
                          sw.ElapsedMilliseconds);

结果:

Iterations UseOutExtensively DontUseOutExtensively
10^7 918 330
10^8 8850 3331
10^9 92009 34823

我们发现执行的操作越多,性能差异就越明显。

Will it hurt performance?

是的,性能差异很小。 IL 编译器和 JIT 编译器可以在这里优化很多东西。例如,采用 UseOutExtensively 方法,没有任何优化的 x64 指令如下所示:

L0000   push    rbp
L0001   sub rsp, 0x30
L0005   lea rbp, [rsp+0x30]
L000a   xor eax, eax
L000c   mov [rbp-0xc], rax
L0010   mov [rbp-4], eax
L0013   mov [rbp+0x10], rcx
L0017   mov [rbp+0x18], rdx
L001b   cmp dword ptr [0x7ff84f32c2f0], 0
L0022   je  short L0029
L0024   call    0x00007ff8adecca10
L0029   nop 
L002a   mov rax, [rbp+0x18]
L002e   xor edx, edx
L0030   mov [rax], edx
L0032   mov [rbp-4], edx
L0035   nop 
L0036   jmp short L0054
L0038   nop 
L0039   mov rax, [rbp+0x18]
L003d   mov eax, [rax]
L003f   mov [rbp-8], eax
L0042   mov rax, [rbp+0x18]
L0046   mov edx, [rbp-8]
L0049   mov [rax], edx
L004b   nop 
L004c   mov eax, [rbp-4]
L004f   inc eax
L0051   mov [rbp-4], eax
L0054   cmp dword ptr [rbp-4], 0x64
L0058   setl    al
L005b   movzx   eax, al
L005e   mov [rbp-0xc], eax
L0061   cmp dword ptr [rbp-0xc], 0
L0065   jne short L0038
L0067   nop 
L0068   add rsp, 0x30
L006c   pop rbp
L006d   ret 

启用优化后,它看起来像这样:

L0000   xor eax, eax
L0002   mov [rdx], eax
L0004   mov ecx, [rdx]
L0006   mov [rdx], ecx
L0008   inc eax
L000a   cmp eax, 0x64
L000d   jl  short L0004
L000f   ret

DontUseOutExtensively,另一方面,优化后看起来像这样。

L0000   xor eax, eax
L0002   inc eax
L0004   cmp eax, 0x64
L0007   jl  short L0002
L0009   mov dword ptr [rdx], 8
L000f   ret 

请注意在使用 out 变量时无法优化的一件事是 mov 指令。使用临时变量时,编译器可以将所有内容保存在用于数学运算的控制寄存器中。设置和访问 out 变量必须将这些值移入和移出调试寄存器,这需要更多时间。

将这些函数插入我手头的 some benchmarking LINQPad code,您可以看到结果在性能上存在可测量的差异,这证实了 E. Shcherbo 指出的结果。

然而,这是一个极其人为的案例。在像您的原始代码这样稍微复杂的东西中,差异变得不那么明显了。

无论如何,您谈论的是数百万次迭代之间的毫秒差异,因此担心出于性能原因采用这些方法中的哪一种几乎肯定是不成熟的优化。

what drawbacks does it have?

忽略性能,您绝对应该考虑此决定如何影响代码的行为

假设您的函数抛出了一个异常,例如:您是否希望在输出中更改 result 的值,即使没有 returned 值?

或者如果作为您的 out 参数传递的变量值正在被其他线程读取怎么办?您是否希望该变量的值随着函数的执行而改变?也许您正在使用该变量来跟踪函数执行的进度,在这种情况下,随时更改值是有价值的。但如果不是,在你有相应的 return 值之前,避免更改 out 变量可能更“整洁”。

关于这一点,为什么要使用 out 参数?您可以使用 ValueTuple、 或仅使用 Nullable<> return 值。这些方法使您的函数 ,这使其对 multi-threaded 操作、LINQ 表达式语法、lambda 表达式等更加友好,具有与临时变量方法相似的性能。

LINQPad Benchmark

static (bool Found, int Result) SomeFunctionValueTuple(int x, int y)
{
    int temp = 8;
    for (int i = 0; i < x; i++)
    {
        temp += temp + x;
        if (temp == y)
        {
            return (false, 0);
        }
    }
    return (true, temp);
}
static int? SomeFunctionNullable(int x, int y)
{
    int temp = 8;
    for (int i = 0; i < x; i++)
    {
        temp += temp + x;
        if (temp == y)
        {
            return null;
        }
    }
    return temp;
}