This question is related to this one but I think should be asked separately.
I have a complex graph of object instances. Now I would like to create a checksum on this object graph directly in memory to detect whether changes have been made to it since the last time the checksum was saved with the object graph. The checksum calculation should be quick and should not consume too much memory.
As I understand now the best solution would probably be to generate a cryptographic key on a binary serialized form of the object graph (correct me if I am wrong). But that comes with a few questions:
- How should I serialize the object? It must be fast and not
consume too much memory. Also it
must reliably always be serialized
the same way. If I use the .NET default serialization can I really be sure that the created binary stream is always the same if the actual data is the same? I doubt it. - So what would be an alternative way to serialize that doesn’t take to long to implement?
Update:
What do you think about this approach:
- navigate through the graph and
foreach object in the graph create a
standard int hashcode using
this algorithm (but exclude reference type members representing nodes in the graph). Add each
hashcode to a integer list - convert the integer list to a byte
array - create a hash on the byte array
using MD5, CRC or similar
The GetHashCode algorithm mentioned should quickly calculate a hashcode that is pretty collision safe for a single object that only takes its primitive members into account. Based on this the byte array should also be a pretty collision safe representation of the object graph and the MD5/CRC hash on this too.
This approach idea is quite near to what I ‘d consider best, but it could use some polishing.
Hashing
Considering that you would prefer speed over accuracy and that an
int-sized hashcode for each item leaves plenty of room for avoiding collissions, the choice of hashcode algo seems right. Excluding reference types that participate in the graph means we ‘re throwing some information away; see below for more on that.Improving the node hash
The idea of not taking into account other nodes connected to the node we are hashing is correct, but maybe we can do better than simply throwing all that information away? We don’t want to take the hashcodes of other nodes into account (they will be hashed themselves as well), but we are throwing away the information provided by the graph edges here: the hashcode for a node with internal data X connected to N other nodes should not be the same for a node with data X connected to M other nodes.
If you have a cheap way of using a part of the edge data into account, use it. For example, if the graph is directed then you can add to the hashcode computed for each node the number of edges going out from it to other nodes.
Aggregating hashcodes
Creating a list of hashcodes would be the middle-ground approach between summing the hashcodes in one
long(very fast and keeps some additional information over summing into anint) and creating a list of hashcodes dependent on a total order of the items in the graph. If you expect lots of items in the graph then summing might be more appropriate (I ‘d try that first and see if it’s collision-free enough); if the graph doesn’t have many items (say < 1000) then I ‘d try the total-order approach first. Remember to allocate enough memory for the list (or simply use an array) when creating it; you already know its final length so that’s a free speed increase.Producing a fixed-size hash
If you have summed the hashcodes into a primitive, this step is not required at all. Otherwise, hashing the list as a
byte[]is what I ‘d consider best. Since hashing the bytes will take very little time in comparison to creating the list, you may want to use a larger-sized hash function than md5 or crc32 to reduce collisions without a practical performance hit.Improving the final hash quality
After getting this “final” hash, I ‘d prepend or append to it the number of items in the hashed graph as fixed-size hex-encoded string because:
Defining a total order
If the order in which the items in the graph are processed is not strictly defined, then the door is open for false negatives: two graphs which should hash to the same value do not because even though they are logically equivalent, the implementation of the hash function chose to process the per-item hashes in a different order. This problem will appear only if you use a list, since addition is transitive so the “add into a
longapproach” is immune to it.To combat that, you need to process the nodes in the graph in a well-defined order. That might be an order that’s easy to produce from the data structure of the nodes (e.g. like preorder traversal on a tree) and/or other information (e.g. class names or node types for each node, node ids if such exist etc).
Since preprocessing the graph to produce a total order is going to take some time, you may want to weigh that against the cost incurred by a false negative result as I mentioned above. Also, if the graphs are large enough then this discussion might be moot because of the node hashcode summation approach being more suited to your needs.