4

I wrote a quick and dirty test to check the performance of Go vs C# in the area of concurrent lookup access and was surprised by the results.

It's a very trivial example and I'm no Go expert but the test is simply to perform 1,000,000 lock/check/add/unlock operations on a map, it's only single-threaded because I'm checking just these functions:

package main

import (
    "fmt"
    "sync"
    "time"
)

var mu sync.Mutex

func main() {
    cache := make(map[int]int, 1000000)
    start := time.Now()

    for i := 0; i < 1000000; i++ {
        mu.Lock()
        if _, ok := cache[i]; ok == false {
            cache[i] = i
        }
        mu.Unlock()
    }

    end := time.Since(start)
    fmt.Println(end)

    var sum int64
    for _, v := range cache {
        sum += int64(v)
    }

    fmt.Println(sum)
}

And the same thing in C# (via LINQPad):

void Main()
{
    var cache = new Dictionary<int, int>(1000000);
    var sw = Stopwatch.StartNew();

    for (var i = 0; i < 1000000; i++)
    {
        lock (cache)
        {
            int d;
            if (cache.TryGetValue(i, out d) == false)
            {
                cache.Add(i, i);
            }
        }
    }

    $"{sw.ElapsedMilliseconds:N0}ms".Dump();

    var sum = 0L;
    foreach (var kvp in cache)
    {
        sum += kvp.Value;
    }
    sum.Dump();
}

I sum the elements of both collections to ensure they match up (499,999,500,000) and print the time taken. Here are the results:

  • C#: 56ms
  • Go: 327ms

I've checked that it's not possible to initialise the size of a map, just the capacity, so I'm wondering if there's anything I could do to improve the performance of the Go map?

It takes Go 32ms to perform 1,000,000 lock/unlock operations without the map access.

Rob
  • 981
  • 12
  • 27
  • Are you using Go 1.5.3? Are you using 64-bit from both Go and C#? – icza Jan 28 '16 at 07:34
  • 2
    Note that `int` is always 32 bit in C# and in case of Go it depends on the architecture, it may be 32 and 64 bit. If it is 64 bit, it may require more work to hash 64 bit and move 64 bit values (map key and map value), further loop variable (`i`) is also `int` so the same applies here too. – icza Jan 28 '16 at 07:51
  • 2
    What are the results without locks/mutexes? If it was Java all these locks would be removed by HotSpot. I suspect C# could do the same. – kostya Jan 28 '16 at 08:32
  • 1
    About 32ms less than with locks. (Tested, not being facetious!) While C# takes ~36ms to complete without locks. – Rob Jan 28 '16 at 09:01
  • 1
    It looks like `map[int]int` implementation is slower than `Dictionary` in this particular test. I wonder how much C# result will change if you use String keys or use different numbers (e.g. `for (var i = 1000000; i < 6000000; i+=5)`). – kostya Jan 28 '16 at 09:24
  • Just out of curiosity, would you try the same test using Mono? I suspect MSFT put some deep Windows kernel hooks into their CLR implementation to make it perform best on Windows. – Mark Richman Jan 28 '16 at 16:21
  • 1
    Good thinking @MarkRichman. Thanks for the suggestion. – Rob Jan 28 '16 at 17:36
  • @Rob yeah, you're not really comparing Go vs. C# here. It's more like the Go runtime vs. Microsoft's CLR (vs. JVM, etc.) – Mark Richman Jan 28 '16 at 18:34
  • Which out-the-box on a Windows machine does appear to offer slower access to maps/Dictionaries (I've run the tests with multiple threads fighting for the collection and the results are pretty linear for both). An interesting test though! If I ever get round to running up a Mono environment, I'll test this and let you know my findings. Thanks again @MarkRichman. – Rob Jan 28 '16 at 18:40
  • 1
    I have Mono on my Mac at home. I can test it later for you and report back. – Mark Richman Jan 28 '16 at 19:05
  • That'd be brilliant, thank you sir! – Rob Jan 28 '16 at 19:10

4 Answers4

6

[S]o I'm wondering if there's anything I could do to improve the performance of the Go map?

No there is not. Go has basically no performance knobs.

(Note that Go's map type is a very general and robust hash map which uses strong cryptographic hashing (if possible) to prevent attacks and forces random key/iteration order. It is "totally general purpose" and not just "a fast dictionary".)

Just to be totally correct: There is the environmental variable GOGC to "tune" GC.

Volker
  • 40,468
  • 7
  • 81
  • 87
  • 2
    Well, if Go's `map` uses cryptographic hashes, that's probably the performance bottleneck right there. Per [the answer](http://stackoverflow.com/a/3893790/467473) to the question "http://stackoverflow.com/questions/3893782/how-is-gethashcode-implemented-for-int32", the .Net hash for an `int` value is the object reference itself: `public override int GetHashCode() { return this; }`. It would be hard to come up with faster hash function. – Nicholas Carey Jun 22 '16 at 21:42
4

There may be one thing that is overlooked and converts the whole exercise into apples and oranges: synchronization. On Go side you use Mutex which goes down to the kernel on every access. On C# side you use lock(){} that uses a combination of SpinLock and only falls back to kernel calls when needed. Since your tests are performed in a single thread anyways, C# never even goes to kernel.

Use of Mutex is discouraged in Go and channels should be used for synchronization instead.

Couple of suggestions: 1. Remove synchronization if you want to benchmark map/dictionary by themselves. 2. Write your tests using correct constructs and paradigms if you like to benchmark concurrent performance.

Cheers!

Monkey
  • 41
  • 1
  • 1
    Go's mutexes are implemented inside the Go runtime and don't use kernel locking mechanisms (since this would block the whole OS thread, preventing other goroutines from running on the thread). Also the use of mutexes is not discouraged per se. If all your problem needs is a mutex, use one. In fact, blindly using channels instead of mutexes, even if a simple mutex could do the job, is discouraged. – nussjustin Feb 12 '19 at 15:09
1

I compiled your C# example using Mono, and ran it on OS X, just to neutralize any "magic" Microsoft might have added to its Windows implementation of Dictionary.

It appears that C# is indeed faster than Go for this particular test, unless there is some Go performance trick we are overlooking:

dict.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;

public class DictionaryTest
{
    public static void Main()
    {
        var cache = new Dictionary<int, int>(1000000);
        var sw = Stopwatch.StartNew();

        for (var i = 0; i < 1000000; i++)
        {
            lock (cache)
            {
                int d;
                if (cache.TryGetValue(i, out d) == false)
                {
                    cache.Add(i, i);
                }
            }
        }

        sw.Stop();

        Console.WriteLine(string.Format("{0}ms", sw.ElapsedMilliseconds));

        var sum = 0L;
        foreach (var kvp in cache)
        {
            sum += kvp.Value;
        }

        Console.WriteLine("Sum: " + sum);
    }
}

If you have the Mono SDK installed, you can compile the above with mcs dict.cs and execute with mono dict.exe.

I ran it several times, and it takes an average of 47ms compared to my average 149ms for the Go version.

Mark Richman
  • 28,948
  • 25
  • 99
  • 159
  • 1
    Hey Mark, thank you for doing that. Volker informs me that the only tweaking available in Go is for the GC (and I doubt that's having much of an effect on a map that's required beyond the scope of the stopwatch functionality). Thanks again sir! – Rob Feb 02 '16 at 06:56
  • You're welcome @Rob I was really hoping to discover that MSFT was pulling a fast one. Ultimately, I don't know that this test actually *proves* anything meaningful. – Mark Richman Feb 02 '16 at 12:19
  • Nope, other than for a very specific and contrived scenario, C# outperforms Go :P – Rob Feb 02 '16 at 13:38
  • 1
    Are there less contrived scenarios where Go always outperforms C#? – Mark Richman Feb 02 '16 at 17:18
  • 3
    Hi @MarkRichman, sorry for the delay in getting back to you on this! I wrote a very simple web service in Go (httprouter) and compared that against .NET's WebApi 2.0 and Go gave C# a thorough spanking. These were just simple GET operations that took no parameters and returned no real data and over 1,000 iterations, Go took 150ms, while C# took 1,900ms. Happy to send you some source code if you'd like to check it out. – Rob Mar 08 '16 at 16:14
  • @Rob that would be great mark [at] markrichman [dot] com thanks! – Mark Richman Mar 08 '16 at 18:53
1

I found if I shink 1000000 to 100000, golang speed would change from 151.0087ms to 10.0005ms (15.1 multiply), while csharp version change from 65ms to 9ms (7.22 multiply) , so it means golang's hashmap has difficulty to handle large map ?

I wrote a simple go benchmark program like this

func BenchmarkIntMapGet100(b *testing.B) {
    count := 100
    setupIntMap(b, count)

    b.ResetTimer()
    for i:=0; i<b.N; i++{
        _, _ = intMap[i%count]
    }
}

and I got the result

BenchmarkIntMapGet10-4          100000000           15.6 ns/op
BenchmarkIntMapGet100-4         100000000           17.1 ns/op
BenchmarkIntMapGet1000-4        50000000            25.7 ns/op
BenchmarkIntMapGet10000-4       50000000            32.3 ns/op
BenchmarkIntMapGet100000-4      30000000            39.2 ns/op
BenchmarkIntMapGet1000000-4     20000000            67.2 ns/op
BenchmarkIntMapGet10000000-4    20000000            82.3 ns/op
redsea
  • 11
  • 2