开发者

Does one of these use more resources than the other?

开发者 https://www.devze.com 2023-03-24 14:02 出处:网络
What is happening differently in the background for these two code blocks?Would one be considered \"better\" than the other?

What is happening differently in the background for these two code blocks? Would one be considered "better" than the other?

My thought is that Example2 might be worse because it might have to wait for the garbage collector to dispose of the item, but I don't know enough about the garbage collector to know if that is true.

Example1:

ListItem item;
for (int i = 1; i <= 32; i++)
{
   item = new ListItem();
   //do some stuff
}开发者_Go百科

Example2:

for (int i = 1; i <= 32; i++)
{
   ListItem item = new ListItem();
   //do some stuff
}


I have copied your code into Visual Studio, compiled it, and then looked at the generated IL. This is the IL generated from Example 1:

.method private hidebysig static void  One() cil managed
{
  // Code size       30 (0x1e)
  .maxstack  2
  .locals init ([0] class WinTest.ListItem item,
           [1] int32 i,
           [2] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  stloc.1
  IL_0003:  br.s       IL_0011
  IL_0005:  nop
  IL_0006:  newobj     instance void WinTest.ListItem::.ctor()
  IL_000b:  stloc.0
  IL_000c:  nop
  IL_000d:  ldloc.1
  IL_000e:  ldc.i4.1
  IL_000f:  add
  IL_0010:  stloc.1
  IL_0011:  ldloc.1
  IL_0012:  ldc.i4.s   32
  IL_0014:  cgt
  IL_0016:  ldc.i4.0
  IL_0017:  ceq
  IL_0019:  stloc.2
  IL_001a:  ldloc.2
  IL_001b:  brtrue.s   IL_0005
  IL_001d:  ret
} // end of method Program::One

And this is the IL generated from Example 2:

.method private hidebysig static void  Two() cil managed
{
  // Code size       30 (0x1e)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] class WinTest.ListItem item,
           [2] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  stloc.0
  IL_0003:  br.s       IL_0011
  IL_0005:  nop
  IL_0006:  newobj     instance void WinTest.ListItem::.ctor()
  IL_000b:  stloc.1
  IL_000c:  nop
  IL_000d:  ldloc.0
  IL_000e:  ldc.i4.1
  IL_000f:  add
  IL_0010:  stloc.0
  IL_0011:  ldloc.0
  IL_0012:  ldc.i4.s   32
  IL_0014:  cgt
  IL_0016:  ldc.i4.0
  IL_0017:  ceq
  IL_0019:  stloc.2
  IL_001a:  ldloc.2
  IL_001b:  brtrue.s   IL_0005
  IL_001d:  ret
} // end of method Program::Two

As far as I understand, they are identical except for the fact that locals are declared (and thus accessed) in reverse order. I don't expect that to have any impact on performance whatsoever.


It depends on what "//do some stuff" stands in for.

In a simple program, both examples would compile to the same MSIL byte code.

But if in the loop an anonymous delegate, perhaps to be executed in another thread, is created that refers to the variable "item", it matters whether "item" was declared inside or outside of the loop. If, as in example 2, "item" is declared inside the loop, then when the delegate runs it will see the value of "item" assigned in the iteration of the loop that created the delegate (which is most likely what is intended in these cases). If, as in example 1, "item" was declared outside the loop, then the delegate will see the value assigned at the time it executes, which may be from a later iteration than the one which created the delegate. This causes confusing race conditions.

Basically, although the MSIL byte code that C# compiles to does not represent the variable scope, the scope is meaningful in C#, and can affect how its various syntactic sugars behave.


Surprisingly, my tests show example 2 to be faster.

Output. Case 1 is single declaration. Case 2 is looped declaration.

Case 1: 10.280418100
Case 2: 10.264818000 99.848254226%
Case 1: 10.592418600
Case 2: 10.140017800 95.729013202%
Case 1: 10.233618000
Case 2: 10.108817800 98.780487996%
Case 1: 10.155617800
Case 2: 10.046417600 98.924731098%
Case 1: 10.503818600
Case 2: 10.246319800 97.548522020%
Case 1: 10.243018400
Case 2: 10.030817600 97.928337217%
Case 1: 10.077617700
Case 2: 10.218017900 101.393188392%
Case 1: 10.303019300
Case 2: 10.526318800 102.167320991%
Case 1: 10.353619900
Case 2: 10.276219400 99.252430544%
Case 1: 10.264818100
Case 2: 10.202417900 99.392096388%

Case 1 Total: 103.007984500
Case 2 Total: 102.060182600 99.079875308%

Code:

Public Sub Main()
    Dim Case1Total As Double = 0
    Dim Case2Total As Double = 0
    For i As Integer = 1 To 10
        Dim Case1 As Double = MeasureTime(AddressOf Case1Method).TotalSeconds
        Case1Total += Case1
        Console.WriteLine("Case 1: {0:N9}", Case1)
        Dim Case2 As Double = MeasureTime(AddressOf Case2Method).TotalSeconds
        Case2Total += Case2
        Console.WriteLine("Case 2: {0:N9} {1:N9}%", Case2, 100 * Case2 / Case1)
    Next i
    Console.WriteLine()
    Console.WriteLine("Case 1 Total: {0:N9}", Case1Total)
    Console.WriteLine("Case 2 Total: {0:N9} {1:N9}%", Case2Total, 100 * Case2Total / Case1Total)
    Console.ReadLine()
End Sub

Private Function MeasureTime(Method As Action) As TimeSpan
    Dim StartTime As Date = Date.Now
    Method()
    Return Date.Now - StartTime
End Function

Private Sub Case1Method()
    Dim o As Label
    For i As Integer = 0 To Limit
        o = New Label
        o.Text = "Label" & i.ToString
        o.Dispose()
    Next
End Sub

Private Sub Case2Method()
    For i As Integer = 0 To Limit
        Dim o As New Label
        o.Text = "Label" & i.ToString
        o.Dispose()
    Next
End Sub

Private Const Limit As Integer = 1024 * 1024


My personal feeling would be that there would be little difference here (beyond the obvious scoping difference).

Either of these will allocate new memory with a reference to it and leave the previous memory with one less reference, ready to be GC'd as/if required.

I don't think that 2 will wait for the GC, partly because the GC isn't "inline", and partly because there's no guarantee that item is the only reference to the instance and require GC anyway - as I say the reference count will be decremented and that's it.

Of course this is just my feeling about it and I'll be interested to see what others have to say (I can feel a few blog posts maybe in the writing!)


When I kept //do some stuff empty, both versions generated exactly the same IL, so there can't be any possible difference there. And I think that if you put some code there, the generated IL would still be the same.

Regarding GC, the second example could look like it might be theoretically faster, because the GC can collect the ListItem before performing the increment and comparison of the loop. But in fact, it can do the same in the first case too, so there is no difference.

The one exception where the two versions aren't equivalent is when using closures. In the first version, there is only one variable “instance” that is closed over, in the second case, there is 32 of them. So there is a performance difference (in the second case, the closure object is created for each iteration), but the difference in meaning is much more important.

In general, I like to keep variables as deep as possible, because I think it help readbality.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号