When we use out or ref inside calculations, with multiple assignments and reading from it, what drawbacks does it have? Will it hurt performance?
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;
}
Or should we better be using additional variable:
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;
}
Update: changed function name from SomeFunction to make it more clear for intended use.
It turns out that the more calculations we do the more the difference between the performance of both.
I believe this is expected, since here we see an extra level of indirection. ldind and stind operations used to get/set the value for out parameter (indirectly) and ldoc with stloc used to get/set values for local variables.
I think that compiler can't do any optimizations here (at least convert UseOutExtensively to DontUseOutExtensively), because this might change the behavior of the method if some other thread writes to the location of out parameter at the same time the function is executed.
Let me a bit simplify your function so that we concentrate on what we're interested in only:
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;
}
So the functions don't do anything useful, they just swap the same value between the variables. Thus we don't have complex additions and conditions, only get/set an out variable and get/set a local variable.
So the test program is the following:
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);
Results:
| Iterations | UseOutExtensively | DontUseOutExtensively |
|---|---|---|
| 10^7 | 918 | 330 |
| 10^8 | 8850 | 3331 |
| 10^9 | 92009 | 34823 |
We see the more operations we perform the more the difference in performance is noticeable.
Will it hurt performance?
There is a very slight performance difference, yes. The IL compiler and the JIT compiler can optimize a lot of things here. For example, taking E. Shcherbo's UseOutExtensively method, the x64 instructions without any optimizations looks like this:
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
Whereas with optimizations enabled, it looks like this:
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, on the other hand, looks like this when optimized.
L0000 xor eax, eax
L0002 inc eax
L0004 cmp eax, 0x64
L0007 jl short L0002
L0009 mov dword ptr [rdx], 8
L000f ret
Notice how one thing that couldn't be optimized when using an out variable is the mov instructions. When using a temporary variable, the compiler can keep everything in Control Registers, which are used for math operations. Setting and accessing out variables have to move these values to and from Debug Registers, which takes a little more time.
Plugging those functions into some benchmarking LINQPad code I have on hand, you can see there's a measurable difference in performance as a result, which confirms the results E. Shcherbo noted.
However, that's an extremely contrived case. In something even slightly more complicated like your original code, the difference becomes far less pronounced.
At any rate, you're talking about a difference of milliseconds over millions of iterations, so worrying about which of these approaches to take for performance reasons is almost certainly a premature optimization.
what drawbacks does it have?
Ignoring performance, you should definitely think about how the behavior of your code is impacted by this decision.
Let's say your function threw an exception, for example: would you want the value of result to have been changed in the output, even though no value was returned?
Or what if the value of the variable passed as your out parameter is being read by other threads? Do you want that variable's value to change as the function executes? Maybe you're using that variable to track the progress of your function's execution, in which case there's value to changing the value as you go. But if not, it's probably "tidier" to avoid changing the out variable until you've got a corresponding return value to go with it.
On that note, why use an out parameter at all? You could use a ValueTuple, as suggested by quaabaam, or just a Nullable<> return value. These approaches make your function pure, which makes it more friendly to multi-threaded operations, LINQ expression syntax, lambda expressions, and so forth, with similar performance to the temporary variable approach.
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;
}