Problem description: We have a rather big application, where we observe high
allocation rate of System.Int32 instances.
With help of Memory Profiler we see small number of long existing Int32
instances (18, to be exact), and 20-25 thousands of Int32 allocations per
seconds.
All those objects are GC collected as Gen0 objects, system has no memory
leaks and can be run for long time.
When memory snapshot is created, GC is executed before snapshot, so snapshot
does not contains any traces of those “temporary” objects.
All our code was specifically written to eliminate boxing whenever possible,
and “by design” we are supposed to not see boxings at all.
So we suspect it is some non-eliminated forgotten boxing in our code, or
boxing caused by third-party component and/or CLR type itself.
System is compiled using VS2008, and uses .Net 3.5 (measurements were done
in both debug and release builds, with the same behavior).
How can we (using windbg, VS2008, Memory Profiler, AQTime or any other
commercially available product) detect why boxing happens ? for example, is
it possible to put a breakpoint on the ctor of boxed int ?
--
Asher.
> How can we (using windbg, VS2008, Memory Profiler, AQTime or any other
> commercially available product) detect why boxing happens ? for example,
> is it possible to put a breakpoint on the ctor of boxed int ?
You might try filtering the output of ildasm for box and unbox
instructions. I've done this before using a very small python script.
As I recall, the .NET CLR contains a number of box and unbox instructions.
Peter
> How can we (using windbg, VS2008, Memory Profiler, AQTime or any other
> commercially available product) detect why boxing happens ? for
> example, is it possible to put a breakpoint on the ctor of boxed int ?
If you dump the MDs of System.Int32 there is no .ctor shown in windbg. But
you can set a breakpoint in the CLR's box routine.
First you have to find the Address of the routine. If you disassemble a
methode where you know that there is a box-instruction, it is easy to
identifiy the method.
example:
static void Main(string[] args) {
Console.ReadLine();
int i = 1;
X(i);
int j = 2;
X(j);
long k = 3;
X(k);
}
static void X(object o) {
o.ToString();
}
IL-Code:
0:000> !dumpil 01532ff0
ilAddr = 00402050
IL_0000: nop
IL_0001: call System.Console::ReadLine
IL_0006: pop
IL_0007: ldc.i4.1
IL_0008: stloc.0
IL_0009: ldloc.0
IL_000a: box System.Int32
IL_000f: call BoxingBreakpointsTest.Program::X
IL_0014: nop
IL_0015: ldc.i4.2
IL_0016: stloc.1
IL_0017: ldloc.1
IL_0018: box System.Int32
IL_001d: call BoxingBreakpointsTest.Program::X
IL_0022: nop
IL_0023: ldc.i4.3
IL_0024: conv.i8
IL_0025: stloc.2
IL_0026: ldloc.2
IL_0027: box System.Int64
IL_002c: call BoxingBreakpointsTest.Program::X
IL_0031: nop
IL_0032: ret
As expected there are three 'box' instructions. Two with an int32 and one
with an Int64. By looking at the (annotated) disassembly you can see the
corresponding CLR-calls:
0:000> !u 01532ff0
Normal JIT generated code
BoxingBreakpointsTest.Program.Main(System.String[])
Begin 01e70070, size bd
01e70070 55 push ebp
01e70071 8bec mov ebp,esp
01e70073 83ec18 sub esp,18h
01e70076 894dfc mov dword ptr [ebp-4],ecx
01e70079 833d142e530100 cmp dword ptr ds:[1532E14h],0
01e70080 7405 je 01e70087
01e70082 e8c2a62578 call mscorwks!JIT_DbgIsJustMyCode (7a0ca749)
01e70087 33d2 xor edx,edx
01e70089 8955f4 mov dword ptr [ebp-0Ch],edx
01e7008c c745ec00000000 mov dword ptr [ebp-14h],0
01e70093 c745f000000000 mov dword ptr [ebp-10h],0
01e7009a 33d2 xor edx,edx
01e7009c 8955f8 mov dword ptr [ebp-8],edx
01e7009f 90 nop
01e700a0 e823439277 call mscorlib_ni+0x6d43c8 (797943c8)
(System.Console.ReadLine(), mdToken: 060007ba)
01e700a5 90 nop
01e700a6 c745f801000000 mov dword ptr [ebp-8],1
01e700ad b94c2c3379 mov ecx,offset mscorlib_ni+0x272c4c (79332c4c)
(MT: System.Int32)
01e700b2 e8651f6bff call 0152201c (JitHelp: CORINFO_HELP_NEWSFAST)
01e700b7 8945e8 mov dword ptr [ebp-18h],eax
01e700ba 8b45e8 mov eax,dword ptr [ebp-18h]
01e700bd 8b55f8 mov edx,dword ptr [ebp-8]
01e700c0 895004 mov dword ptr [eax+4],edx
01e700c3 8b4de8 mov ecx,dword ptr [ebp-18h]
01e700c6 ff1504305301 call dword ptr ds:[1533004h]
(BoxingBreakpointsTest.Program.X(System.Object), mdToken: 06000002)
01e700cc 90 nop
01e700cd c745f402000000 mov dword ptr [ebp-0Ch],2
01e700d4 b94c2c3379 mov ecx,offset mscorlib_ni+0x272c4c (79332c4c)
(MT: System.Int32)
01e700d9 e83e1f6bff call 0152201c (JitHelp: CORINFO_HELP_NEWSFAST)
01e700de 8945e8 mov dword ptr [ebp-18h],eax
01e700e1 8b45e8 mov eax,dword ptr [ebp-18h]
01e700e4 8b55f4 mov edx,dword ptr [ebp-0Ch]
01e700e7 895004 mov dword ptr [eax+4],edx
01e700ea 8b4de8 mov ecx,dword ptr [ebp-18h]
01e700ed ff1504305301 call dword ptr ds:[1533004h]
(BoxingBreakpointsTest.Program.X(System.Object), mdToken: 06000002)
01e700f3 90 nop
01e700f4 c745ec03000000 mov dword ptr [ebp-14h],3
01e700fb c745f000000000 mov dword ptr [ebp-10h],0
01e70102 b98c223379 mov ecx,offset mscorlib_ni+0x27228c (7933228c)
(MT: System.Int64)
01e70107 e8101f6bff call 0152201c (JitHelp: CORINFO_HELP_NEWSFAST)
01e7010c 8945e8 mov dword ptr [ebp-18h],eax
01e7010f 8b45e8 mov eax,dword ptr [ebp-18h]
01e70112 8b4dec mov ecx,dword ptr [ebp-14h]
01e70115 8b55f0 mov edx,dword ptr [ebp-10h]
01e70118 894804 mov dword ptr [eax+4],ecx
01e7011b 895008 mov dword ptr [eax+8],edx
01e7011e 8b4de8 mov ecx,dword ptr [ebp-18h]
01e70121 ff1504305301 call dword ptr ds:[1533004h]
(BoxingBreakpointsTest.Program.X(System.Object), mdToken: 06000002)
01e70127 90 nop
01e70128 90 nop
01e70129 8be5 mov esp,ebp
01e7012b 5d pop ebp
01e7012c c3 ret
You can see that the 'call 0152201c' instruction every time a box IL
instruction is executed, so this would be a good candidate to start:
bp 0152201c
Maybe this method is used for other things but boxing (it looks like the
general 'NewObject' method), so you may set a conditional breakpoint
depending if there is 79332c4c (MT for Int32) in ecx:
bp 0152201c ".if @ecx=79332c4c {} .else {gc}"
This statement adds a breakpoint when an System.Int32 is created. Within the
sample it doesn't break on boxing the Int64. The address may be the same on
your system, but it may also variy.
you can put a command between {...} if you want something to be executed
when it is hit.
BTW:
The Address '0152201c' seems to be a little bit "strange", because it is not
within the range of mscorwks. !vmmap shows it like:
01522000-01522fff 00001000 NA ExRdWr Commit Private
It's contained in an internal CLR heap of the System Domain:
0:000> !EEHeap
Loader Heap:
--------------------------------------
System Domain: 7a3bd058
LowFrequencyHeap: Size: 0x0(0)bytes.
HighFrequencyHeap: 01522000(8000:1000) Size: 0x1000(4096)bytes.
Maybe some Code that is emitted from the JITer.
OK?
GP
With your instructions I was able to locate where the boxing occurs.
I was able to verify my conclusions using reflector.
Apparently, calling DateTime.Now will cause boxing!!
Calling DateTime.Now is actually DateTime.UtcNow.ToLocalTime()
The implementation of the ToLocalTime() method finnaly calls the
GetDaylightChanges method implementd in CurrentSystemTimeZone class.
The implementation of this class uses boxing. This is bad.
I will try contacting the BCL team with this issue.
Thank you for your help!
--
Asher.
> Apparently, calling DateTime.Now will cause boxing!!
> Calling DateTime.Now is actually DateTime.UtcNow.ToLocalTime()
> The implementation of the ToLocalTime() method finnaly calls the
> GetDaylightChanges method implementd in CurrentSystemTimeZone class.
> The implementation of this class uses boxing.
Yes. I see. The 'year' argument is boxed to an object, because it used a
'System.Collections.Hashtable'.
> This is bad.
I agree. Specially based on the facted that DateTime.Now maybe called very
frequently (some logging or so).
> I will try contacting the BCL team with this issue.
There is still some BCL code relying on the Collection Classes from 1.0
(none-generic), so boxing is not avoidable when dealing with value types.
Replacing the Hashtable with a Dictionary<int, DaylightTime> shall do the
job.
Sometimes it's questionable how the priorities are set at MS. There is so
much effort for new Frameworks (which is also good IMO), but the foundation
classes (mscorlib, system, system.data, ...) also shall get the needed
priorities. Have you checked in available .NET 4.0 builds? Anything changed
in this specific case?
You may use UTCNow and perform the .ToLocal conversation by yourself. Maybe
by caching the needed offsets you get from the BCL Classes. Depending on how
sofisticated you need it the can be from 10->1000 lines of code ;-)
> Thank you for your help!
You'r welcome.
GP