4

I read this question and another question. In one of these two questions, I also read that Guid Structure consists of following four fields: Int32, Int16, Int16 and Byte[8], so the comparison between two Guid should be faster.

Well, I use the Guid Structure only to generate UUID, and then I have to compare only those previously generated UUID. Therefore, I would like to convert each UUID generated quickly in a format comparable.

I ran some tests using the following code (I took inspiration from this question).

        struct FastUuid
        {
            public long _part1;
            public long _part2;

            public FastUuid(byte[] value)
            {
                _part1 = BitConverter.ToInt64(value, 0);
                _part2 = BitConverter.ToInt64(value, 8);
            }
        }

        static void Main(string[] args)
        {
            TestUuidCompare(1000000000);
            Console.ReadLine();

        }

        public static void TestUuidCompare(uint numberOfChecks)
        {
            Guid uuid1 = Guid.NewGuid();
            Guid uuid2 = new Guid(uuid1.ToByteArray());

            byte[] a1 = uuid1.ToByteArray();
            byte[] a2 = uuid2.ToByteArray();

            string s1 = uuid1.ToString();
            string s2 = uuid2.ToString();

            BigInteger b1 = new BigInteger(uuid1.ToByteArray());
            BigInteger b2 = new BigInteger(uuid2.ToByteArray());

            long l1part1 = BitConverter.ToInt64(uuid1.ToByteArray(), 0); // Parts 1 and 2
            long l1part2 = BitConverter.ToInt64(uuid1.ToByteArray(), 8); // of uuid1.

            long l2part1 = BitConverter.ToInt64(uuid2.ToByteArray(), 0); // Parts 1 and 2
            long l2part2 = BitConverter.ToInt64(uuid2.ToByteArray(), 8); // of uuid2.

            long[] la1 = { l1part1, l1part2 }; // Parts 1 and 2 of uuid1.
            long[] la2 = { l2part1, l2part2 }; // Parts 1 and 2 of uuid2.

            int i1part1 = BitConverter.ToInt32(uuid1.ToByteArray(), 0);  // Parts 1, 2, 3
            int i1part2 = BitConverter.ToInt32(uuid1.ToByteArray(), 4);  // and 4
            int i1part3 = BitConverter.ToInt32(uuid1.ToByteArray(), 8);  // of
            int i1part4 = BitConverter.ToInt32(uuid1.ToByteArray(), 12); // uuid1.

            int i2part1 = BitConverter.ToInt32(uuid2.ToByteArray(), 0);  // Parts 1, 2, 3
            int i2part2 = BitConverter.ToInt32(uuid2.ToByteArray(), 4);  // and 4
            int i2part3 = BitConverter.ToInt32(uuid2.ToByteArray(), 8);  // of
            int i2part4 = BitConverter.ToInt32(uuid2.ToByteArray(), 12); // uuid2

            FastUuid fast1 = new FastUuid(uuid1.ToByteArray());
            FastUuid fast2 = new FastUuid(uuid2.ToByteArray());


            // UUID are equal (worse scenario)

            Stopwatch sw = new Stopwatch();
            bool result;

            sw.Reset(); sw.Start();
            for (int i = 0; i < numberOfChecks; i++)
            {
                result = (uuid1 == uuid2);
            }
            Console.WriteLine("- Guid.Equals: \t{0}", sw.Elapsed);

            sw.Reset(); sw.Start();
            for (int i = 0; i < numberOfChecks; i++)
            {
                result = Array.Equals(a1, a2);
            }
            Console.WriteLine("- Array.Equals(byte): \t{0}", sw.Elapsed);

            sw.Reset(); sw.Start();
            for (int i = 0; i < numberOfChecks; i++)
            {
                result = s1.Equals(s2);
            }
            Console.WriteLine("- String.Equals: \t{0}", sw.Elapsed);

            sw.Reset(); sw.Start();
            for (int i = 0; i < numberOfChecks; i++)
            {
                result = b1.Equals(b2);
            }
            Console.WriteLine("- BigInteger.Equals: \t{0}", sw.Elapsed);

            sw.Reset(); sw.Start();
            for (int i = 0; i < numberOfChecks; i++)
            {
                result = (l1part1 == l2part1 && l1part2 == l2part2);
            }
            Console.WriteLine("- Two long compare: \t{0}", sw.Elapsed);

            sw.Reset(); sw.Start();
            for (int i = 0; i < numberOfChecks; i++)
            {
                result = Array.Equals(la1, la2);
            }
            Console.WriteLine("- Array.Equals(long): \t{0}", sw.Elapsed);

            sw.Reset(); sw.Start();
            for (int i = 0; i < numberOfChecks; i++)
            {
                result = (i1part1 == i2part1 && i1part2 == i2part2 && i1part3 == i2part3 && i1part4 == i2part4);
            }
            Console.WriteLine("- Four int compare: \t{0}", sw.Elapsed);

            sw.Reset(); sw.Start();
            for (int i = 0; i < numberOfChecks; i++)
            {
                result = fast1.Equals(fast2);
            }
            Console.WriteLine("- FastUuid: \t{0}", sw.Elapsed);
        }

With the following results.

  • Guid.Equals: 18.911 s
  • Array.Equals(byte): 12.003 s
  • String.Equals: 26.159 s
  • BigInteger.Equals: 22.652 s
  • Two long compare: 6.530 s <--- the fastest
  • Array.Equals(long): 11.930 s
  • Four int compare: 6.795 s
  • FastUuid: 1m 26.636 s <--- the slowest

Why the FastUuid comparison is the slowest? Since the UUID should be the key to a Dictionary, is there a way to get the performance comparison between two long, and at the same time, implement the UUID with a small (16 byte) object/struct?

EDIT: Actually, the tests that I performed measure the performance of the comparison between two UUID, so they have little meaning to evaluate the performance of accessing a Dictionary, because when I invoke the ContainsKey method, it calculates the hash value of the UUID. Should I evaluate the performance in calculating the hash value of the Guid, String, FastUuid, etc.? How does the ContainsKey method work?

Community
  • 1
  • 1
enzom83
  • 8,080
  • 10
  • 68
  • 114
  • 4
    To the person who downvoted the OP's question, why? Constructive feedback is always welcomed! – Will Feb 02 '12 at 00:06
  • 7
    Your test is quite questionable. You are testing the extremely unlikely case of having 1000000000 equal guids. Guid.Equals() is optimized for the far more likely case that they *won't* be equal. – Hans Passant Feb 02 '12 at 00:11
  • I set 1000000000 checks in order to get a better comparison. – enzom83 Feb 02 '12 at 01:29

2 Answers2

5

The default implementation of Equals for structs uses reflection for comparison, which is slower. (See more at ValueType.Equals on MSDN.)

You should override Equals and provide your own implementation:

public override bool Equals(object other)
{
    var fastOther = other as FastUuid?;
    return fastOther.HasValue &&
        fastOther.Value._part1 == _part1 &&
        fastOther.Value._part2 == _part2;
}

This won't fix the problem entirely, however. Since this is a struct you should also implement IEquatable<FastUuid> to avoid having to box/unbox the other item:

public bool Equals(FastUuid other)
{
    return
        other._part1 == _part1 &&
        other._part2 == _part2;
}

Equals is then just:

public override bool Equals(object other)
{
    var fastOther = other as FastUuid?;
    return fastOther.HasValue && Equals(fastOther.Value);
}
porges
  • 30,133
  • 4
  • 83
  • 114
  • Using this solution, the elapsed time increases: 3m 52 s – enzom83 Feb 02 '12 at 00:25
  • 2
    Sorry, I should also have shown how to avoid boxing the other struct. I've updated the example. – porges Feb 02 '12 at 00:31
  • It should be faster than the Array.Equals version, at least. Are you on 64- or 32-bit? – porges Feb 02 '12 at 00:49
  • For the second version I gave, I get about the same performance as Array.Equals in 32-bit. – porges Feb 02 '12 at 01:05
  • Yes, I noticed that I get more or less the same performance of Array.Equals. Probably the direct comparison between two long is faster because it is not necessary to invoke a `Equals` method. – enzom83 Feb 02 '12 at 01:17
  • Maybe I should run tests comparing the speed of access to a `Dictionary<,>`: I added some detail to the end of the question. – enzom83 Feb 02 '12 at 01:20
  • I'm guessing that the performance difference for "plain ints" is due to some optimization that the compiler can perform then that it can't for the Equals method. If you want to use Dictionary you should override GetHashCode. – porges Feb 02 '12 at 01:23
  • 3
    Notice that this is no longer true with .Net 4.5: "If none of the fields of the current instance and obj are reference types, the Equals method performs a byte-by-byte comparison of the two objects in memory. Otherwise, it uses reflection to compare the corresponding fields of obj and this instance." - http://msdn.microsoft.com/en-us/library/2dts52z7.aspx – Peter Feb 21 '14 at 22:03
  • Is there any consensus on whether this code is still faster? I'm getting a Guid comparison performance problem, and I'm investigating options, but I'm using the latest version of .NET so I'm wondering if the old issue is still relevant. – Christian Findlay Jan 02 '18 at 01:58
3

Structs use runtime reflection to determine what needs to be compared. Override Equals with your own comparison method to get much faster results.

see http://msdn.microsoft.com/en-us/library/2dts52z7.aspx

Update - Overriding equals for a struct ends up having boxing overheads (converting struct to object and back to struct), but if you define an Equals that takes the FastUuid struct directly, it runs much faster:

        public bool Equals(FastUuid value)
        {
            return this._part1 == value._part1
                && this._part2 == value._part2;
        }

This runs slightly faster than the Guid implementation on my box (4.2sec vs 5.8sec, but still slower than long comparison at 1.7sec).

ps. Pls be careful to run tests with release builds only, as debug builds can seriously affect timing.

Will
  • 2,512
  • 14
  • 19
  • Good! In release mode, these times decreases: - Two long compare: 2.291 s - Four int compare: 2.245 s - FastUuid: 10.018 s – enzom83 Feb 02 '12 at 01:10