.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;
}
}
random/net_sizeof_reference_type.txt · Last modified: 2011/07/12 09:51 by grant