.NET Sizeof Reference Type
//
// Written by Grant Jenks
// http://www.grantjenks.com/
//
// DISCLAIMER
// THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
// LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
// OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND,
// EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
// ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.
// SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY
// SERVICING, REPAIR OR CORRECTION.
//
// Copyright
// This work is licensed under the
// Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.
// To view a copy of this license, visit
// http://creativecommons.org/licenses/by-nc-nd/3.0/
// or send a letter to Creative Commons, 171 Second Street, Suite 300,
// San Francisco, California, 94105, US.A
//
// Description
// Recently I wondered about the size of a LinkedListNode<T> object. As I
// thought about it, I also wondered how large an Object was in .NET. I
// wrote this program as a means of estimating the size of a reference type.
// After reading around a bit online, it was clear that C# doesn't make
// this easy. The language's built-in "sizeof" operator works only on value
// types. In the forums and blogs, people suggested adding the size of all
// the fields using reflection or peeking at the object in a debugger /
// profiler. The first idea that came to me was neither of these options.
//
// In Windows, a program's "Working Set" is the amount of memory the system
// has committed to a process. This is generally a very good measure of how
// much actual memory an application uses. With a measure of the amount of
// committed memory, I devised the following solution:
// 1. Measure the program's "Working Set"
// 2. Allocate a lot of objects.
// 3. Measure the "Working Set" again.
// 4. Divide the difference by the number of allocated objects.
// This method gives a rough estimate a reference type's size. It also
// illustrates the amount of overhead the CLR requires for allocating objects.
//
// Data
// Measuring Object:
// iter working set size estimate
// -1 11190272
// 1000000 85995520 74.805248
// 2000000 159186944 73.998336
// 3000000 231473152 73.4276266666667
// 4000000 306401280 73.802752
// 5000000 379092992 73.580544
// 6000000 451387392 73.3661866666667
// 7000000 524378112 73.3125485714286
// 8000000 600096768 73.613312
// 9000000 676405248 73.9127751111111
// Average size: 73.7577032239859
// Measuring LinkedListNode<Object>:
// iter working set size estimate
// -1 34168832
// 1000000 147959808 113.790976
// 2000000 268963840 117.397504
// 3000000 387796992 117.876053333333
// 4000000 507973632 118.4512
// 5000000 628379648 118.8421632
// 6000000 748834816 119.110997333333
// 7000000 869265408 119.299510857143
// 8000000 993509376 119.917568
// 9000000 1114038272 119.985493333333
// Average size: 118.296829561905
// Estimated Object size: 29.218576886067
// Estimated LinkedListNode<reference type> size: 44.5391263379189
//
// Analysis
// The average size of allocating millions of Objects is approximately 29.2
// bytes. A LinkedListNode<reference type> object is approximately 44.5 bytes.
// This data illustrates two things:
// 1. It's very unlikely that the system is allocating a partial byte. The
// fractional measure of bytes indicates the overhead the CLR requires to
// allocate and track millions of reference types.
// 2. If we simply round-down the number of bytes, we're still unlikely to
// have the proper byte count for reference types. This is clear from the
// measure of Objects. If we round down, we assume the size is 29 bytes
// which, while theoretically possible, is unlikely because of padding.
// In order to improve performance, object allocations are usually padded
// for alignment purposes. I would guess that CLR objects will be 4 byte
// aligned.
// Assuming CLR overhead and 4-byte alignment, I'd estimate an Object in C# is
// 28 bytes and a LinkedListNode<reference type> is 44 bytes.
//
using System;
using System.Diagnostics;
using System.Collections.Generic;
class CSharpSizeofReference
{
delegate void Allocator<T>(LinkedList<T> list, int iter);
static void AllocateReferenceType<T>(LinkedList<T> list, int iter) where T : new()
{
for (int i = 0; i < iter; i++)
{
list.AddLast(new T());
}
}
static void AllocateLinkedListNodes(LinkedList<LinkedListNode<Object>> list, int iter)
{
for (int i = 0; i < iter; i++)
{
list.AddLast(new LinkedListNode<Object>(new Object()));
}
}
static double Measure<T>(Allocator<T> allocator)
{
Console.WriteLine("iter\tworking set\tsize estimate");
GC.Collect();
double workingSet = GetWorkingSet();
Console.WriteLine("-1\t{0}", workingSet);
double[] sizes = new double[9];
for (int i = 0; i < 9; i++)
{
GC.Collect();
int count = 1000000 * (i + 1);
LinkedList<T> list = new LinkedList<T>();
allocator(list, count);
double newWorkingSet = GetWorkingSet();
double allocated = newWorkingSet - workingSet;
double size = allocated / count;
Console.WriteLine("{0}\t{1}\t{2}", count, newWorkingSet, size);
sizes[i] = size;
}
double sum = 0;
foreach (double size in sizes)
{
sum += size;
}
Console.WriteLine("Average size: {0}", (sum / 9));
return (sum / 9);
}
static long GetWorkingSet()
{
Process[] processes = Process.GetProcessesByName("CSharpSizeofReference");
if (processes.Length != 1)
{
throw new Exception("Expected exactly one CSharpSizeofReference process.");
}
return processes[0].WorkingSet64;
}
static int Main(String[] args)
{
Console.WriteLine("Measuring Object:");
double llobj = Measure<Object>(AllocateReferenceType<Object>);
Console.WriteLine("Measuring LinkedListNode<Object>:");
double llllobj = Measure<LinkedListNode<Object>>(AllocateLinkedListNodes);
double ll = llllobj - llobj;
double obj = llobj - ll;
Console.WriteLine("Estimated Object size: {0}", obj);
Console.WriteLine("Estimated LinkedListNode<reference type> size: {0}", ll);
return 0;
}
}