How deep is your clone?

July 30, 2008 12:35 by Andre Loker

Someone asked in a forum:

Why is there no ICloneable<T> only ICloneable?

Good question. It wouldn't have taken too much effort to introduce a generic version when .NET 2.0 came out. But I think MS had a good reason not to introduce ICloneable<T>. And that's because they realized that ICloneable sucks in the first place! Why is that? Because it is effectively undefined what cloning does.

Let's have a look at the MSDN library. It has this to say about ICloneable:

Supports cloning, which creates a new instance of a class with the same value as an existing instance.

And this about ICloneable.Clone:

Creates a new object that is a copy of the current instance.

...

Clone can be implemented either as a deep copy or a shallow copy. In a deep copy, all objects are duplicated; whereas, in a shallow copy, only the top-level objects are duplicated and the lower levels contain references.

All right, doesn't sound too bad, does it. There are two interesting points, though:

Point 1: Clone returns a new object. Really? Not necessarily. System.String implements ICloneable.Clone like this:

   1: public object Clone(){
   2:   return this;
   3: }

Not necessarily problematic, as strings are immutable, but still explicitly against the documentation of ICloneable.Clone.

Point 2: shallow vs. deep copy. This is hell, trust me. Any implementor of ICloneable is free to choose "how deeply" it copies itself. This can give a multitude of different meanings to Clone().

Let us have a look at an example of shallow copy. To implement Clone as a shallow copy method create a new instance of the class and set all instance variables to the value of the original class. This is in fact what Object.MemberwiseClone() does, so let's just use that:

   1: class Place {
   2:   public string Name { get; set; }
   3:   public string Postcode { get; set; }
   4: }
   5:  
   6: class Address {
   7:   public string Street { get; set; }
   8:   public string HouseNumber { get; set; } // string to support '23b'
   9:   public Place Place { get; set; }
  10: }
  11:  
  12: class Order : ICloneable {
  13:   public Address ShippingAddress { get; set; }
  14:  
  15:   public object Clone() {
  16:     // shallow copy
  17:     return MemberwiseClone();
  18:   }
  19: }

It's easy to do. MemberwiseClone() creates a new instance of Order. The returned instance uses the same Address object as the original Order. That's fine until someone does this:

   1: Order order = // order from database 
   2: Order similarOrder = (Order) order.Clone();
   3: similarOrder.ShippingAddress.Street = "Somewhere";
   4: similarOrder.ShippingAddress.HouseNumber = "12c";

By changing the Address instance of similarOrder (which is the same as order.Address) we changed the shipping address of the original order. Whoops. Might be better to do a deep copy! Here's the modified code that does a deep copy:

   1: class Place : ICloneable {
   2:   public string Name { get; set; }
   3:   public string Postcode { get; set; }
   4:  
   5:   public object Clone() {
   6:     // shallow copy is enough here
   7:     return MemberwiseClone();
   8:   }
   9: }
  10:  
  11: class Address : ICloneable {
  12:   public string Street { get; set; }
  13:   public string HouseNumber { get; set; } // string to support '23b'
  14:   public Place Place { get; set; }
  15:  
  16:   public object Clone() {
  17:     return new Address() {
  18:       Street = Street,
  19:       HouseNumber = HouseNumber,
  20:       Place = (Place) Place.Clone()
  21:     };
  22:   }
  23: }
  24:  
  25: class Order : ICloneable {
  26:   public Address ShippingAddress { get; set; }
  27:  
  28:   public object Clone() {
  29:     // deep copy
  30:     return new Order {
  31:       ShippingAddress = (Address)ShippingAddress.Clone()
  32:     };
  33:   }
  34: }

Now we are on the safe side. We can mess with the address of a cloned order anyway we like without affecting the original order. But wait... all of a sudden we realize that the postcode of the address was wrong, so we fix that:

   1: Order order = // order from database 
   2: Order similarOrder = (Order)order.Clone();
   3: similarOrder.ShippingAddress.Place.Postcode = "1234";

But now we have a new problem: by doing a deep copy we duplicated the Place instance as well. If we change the postcode in one of the instances, it won't affect the other one - but it should! So what we actually need in this case is a semi-deep copy. Some parts of the object graph have to be copied deeply (the Address), some parts need a shallow copy (the Place). There's clearly no general pattern in this.

While this example is a bit made up you might find such situations in practice. Sometimes you won't have any chance to avoid it. But you see that it can get complicated. More complicated than a single one-size-fits-all interface like ICloneable could handle. In general "cloning" an object is by far not as transparent as ICloneable.Clone might suggest. If you need some sort of copying function, go ahead. Give it a clear name and implement it in a reasonable way. But don't implement ICloneable as it can rise false assumptions.

MS probably realized this problem. They did not want to advertise "general purpose cloning" by introducing another ICloneable interface which would make cloning even more convenient for the user.


Effizienz primitiver Datentypen im ControlState

February 25, 2008 22:10 by Andre Loker

ASP.NET 2 bietet neben dem ViewState auch den so genannten ControlState an, um Control-Zustände über Postbacks hinweg zu speichern. In der Standardeinstellung wird der ControlState wie der ViewState in das versteckte Feld "__VIEWSTATE" der Seite serialisiert. Informationen dazu, wie man ControlStates verwendet, gibt es genügend im Internet.

Durch Zufall bin ich heute aber auf eine interessante Tatsache gestoßen. Natürlich sollten die Daten des ControlStates so klein wie möglich gehalten werden. In meinem konkreten Fall musste das Control lediglich die ID aus einer Datenbank speichern. Der Wert war vom Typ Int64, was 8 bytes zur Laufzeit entspricht. Ich erwartete also etwas mehr als diese 8 bytes im ControlState. Zur Kontrolle habe ich mir trace.axd zur Hilfe genommen, um die Größe des ControlStates zu überprüfen - und war doch ziemlich überrascht, dass dieser eine Int64 sage und schreibe 132 bytes veranschlagte. Das schien mir doch reichlich viel und veranlasste mich dazu, genauer zu hinterfragen, warum so der Wert so groß ist.

Versuchsweise verwendete ich statt des Int64 einen "gewöhnlichen" Int32 und erhielt als ControlState Größe nunmehr lediglich 8 bytes. Mir wurde dann relativ schnell klar, dass die Wahl des Datentyps von entscheidender Bedeutung ist. Ich habe also eine Testreihe gestartet, in der ich verschiedenste primitive Datentypen als ControlState verwendete. Dabei wurde auch deutlich, dass nicht nur der Datentyp eine Rolle spielte, sondern auch der Wert(!). Kleine Integer wurden offenbar kompakter serialisiert als große.

Ergebnisse

Ganzzahlige und Fließkommadatentypen

Die erste "Disziplin" ist das Speichern einfacher numerischer Datentypen, sowohl ganzzahlige Typen als auch Fließkommatypen. Getestet habe ich einmal den Wert "0" für jeden Datentyp und den höchsten Wert, welcher durch das konstante Feld MaxValue bei allen Typen angegeben ist.

Datentyp ControlState:
Wert 0
ControlState:
MaxValue
byte 8 8
sbyte 132 136
short 12 12
ushort 132 140
int 8 16
uint 132 144
long 132 156
ulong 132 160
float 12 12
double 20 20
decimal 136 172

 

Ergebnis: Byte, short, int float und double werden gesondert behandelt und sehr effizient gespeichert. Alle vorzeichenlosen Ganzzahlen (Ausnahme: bytes - dort sind es die vorzeichenbehafteten) sowie long, ulong und decimal sind "teuer". Für sie besteht offensichtlich keine effiziente Serialisierung im Framework.

 

Strings und Booleans

Strings und Boolsche Werte.

Wert ControlState
true 8
false 8
"test" 16
string.Empty 8

 

Ergebnis: sowohl Boolsche Werte als auch Strings werden sehr effizient serialisiert.

 

arrays

Falls man mehrere Werte als ControlState speichern möchte, wäre es ggf. interessant, diese als Array zu serialisieren. Ich habe einfache object-Arrays getestet als auch einige spezialisierte arrays (byte und int).

Array inhalt ControlState:
Wert 0 bzw String.Empty
ControlState:
MaxValue bzw. "test
"
object[] { int } 12 20
object[] { int, int } 16 28
object[] { int, int, int } 16 36
object[] { int, int, int, int } 16 44
object[] { byte } 16 16
byte[] { byte } 132 132
int[] { int } 12 20
string[] {string } 12 16

 

Ergebnis: object-Arrays scheinen sehr kompakt gespeichert zu werden. Ebenso sind int und string Arrays sehr effizient. Andere Arraytypen scheinen nicht gesondert behandelt zu werden.

 

8 bytes als speichern

In meinem konkreten Beispiel möchte ich einen Int64 als ControlState speichern. Die folgende Tabelle zeigt, wie ich dieses 8 bytes ggf. auch als ein Array von kleineren Zahlenwerten speichern könnte, indem ich die 8 bytes aufteile.

Array ControlState:
Wert 0
ControlState:
MaxValue
object[] { 8x byte } 32 32
object[] { 4x short } 28 28
object[] { 2x int } 16 28

 

Ergebnis: wie schon zuvor siegt der Int32-Typ. Doch selbst, wenn der Int64 Wert als ein Array von 8 bytes gespeichert wird, beträgt die Größe höchstens ein Viertel im Vergleich zum serialisierten Int64

 

Pair und Triplet

Als Alternative zum Array bieten sich für ControlStates, die aus zwei oder drei Werten bestehen, die Typen Pair und Triplet im System.Web.UI Namespace an. Die folgende Tabelle zeigt die ControlState Größe bei Verwendung des Pairs oder Triplets und zum Vergleich dazu noch einmal gleichwertige Arrays mit zwei bzw. drei Werten.

Inhalt ControlState:
Wert 0
ControlState:
MaxValue
Pair(int, int) 12 24
object[] { int, int } 16 28
Triplet(int, int, int) 12 32
object[] { int, int, int } 16 36

 

Ergebnis: offensichtlich sind Pair und Triplet nochmal jeweils 4 bytes kleiner als ihr Array-Pendant.

Fazit

Es ist offensichtlich, dass einige Datentypen gesondert serilisiert werden. Mit einer klugen Auswahl des Datentypes, kann die Größe des ControlStates dramatisch verkleinert werde, je nach Fall zwischen 75% und 90%.

Einige Richtlinien, die man aus den Tabellen entnehmen kann:

  • int (Int32) ist generell der bevorzugte Datentyp für Ganzzahlen.
  • Im ControlState zu empfehlen sind nur folgende primitive Typen: byte, short, int, float, double, bool und string.
  • Um zwei oder drei Werte zu speichern, sollten Pair bzw. Triplet verwendet werden
  • Bei mehr als drei Werten können Arrays verwendet werden. In der Regel sollte das ein object[] Array sein. Bei int und string sind int[] respektive string[] gleich effizient gegenüber object[].
  • 64 bit Integer sollten zum Speichern mittels Bitmaske in 2 32 bit Integer geteilt und als Pair gespeichert werden
  • sbyte, ushort und ulong sollten zunächst auf Bitniveau in die effizienten Pendants byte, short und long gebracht werden.