out 和 ref 可以用作临时变量吗?
Can out and ref be used as temporary variables?
当我们在计算中使用out或ref时,多次赋值并从中读取,它有什么缺点?会影响性能吗?
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 表达式等更加友好,具有与临时变量方法相似的性能。
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;
}
当我们在计算中使用out或ref时,多次赋值并从中读取,它有什么缺点?会影响性能吗?
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 表达式等更加友好,具有与临时变量方法相似的性能。
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;
}