go maps non-performant for large number of keys











up vote
-2
down vote

favorite












I discovered very strange behaviour with go maps recently. The use case is to create a group of integers and have O(1) check for IsMember(id int).



The current implementation is :



func convertToMap(v int64) map[int64]void {
out := make(map[int64]void, len(v))
for _, i := range v {
out[i] = void{}
}
return out
}

type Group struct {
members map[int64]void
}

type void struct{}

func (g *Group) IsMember(input string) (ok bool) {
memberID, _ := strconv.ParseInt(input, 10, 64)
_, ok = g.members[memberID]
return
}


When i benchmark the IsMember method, until 6 million members, everything looks fine. But above that the map look up is taking 1 second for each lookup!!



The benchmark test:



func BenchmarkIsMember(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
g := &Group{}
g.members = convertToMap(benchmarkV)

for N := 0; N < b.N && N < sizeOfGroup; N++ {
g.IsMember(benchmarkKVString[N])
}
}

var benchmarkV, benchmarkKVString = func(size int) (int64, string{
v := make(int64, size)
s := make(string, size)
for i := range v {
val := rand.Int63()
v[i] = val
s[i] = strconv.FormatInt(val, 10)
}
return v, s
}(sizeOfGroup)


Benchmark numbers:



const sizeOfGroup  = 6000000
BenchmarkIsMember-8 2000000 568 ns/op 50 B/op 0 allocs/op

const sizeOfGroup = 6830000
BenchmarkIsMember-8 1 1051725455 ns/op 178767208 B/op 25 allocs/op


Anything above group size of 6.8 million gives the same result.



Can someone help me to explain why this is happening, and can anything be done to make this performant while still using maps?



Also, i dont understand why so much memory is being allocated? Even if the time taken is due to collision and then linked list traversal, there shouldn't be any mem allocation, is my thought process wrong?










share|improve this question
























  • Your code doesn't compile.
    – peterSO
    Nov 22 at 5:29






  • 1




    i dont understand why so much memory is being allocated. That s a subtle statement given that you have written make(map[int64]void, len(v)). see also play.golang.org/p/JEEI4qkfYyn
    – mh-cbon
    Nov 22 at 5:57










  • @mh-cbon if this was the case, then the benchmark for 6 million should have shown the same mem allocation as well. The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well
    – zaRRoc
    Nov 22 at 7:44










  • @mh-cbon nevermind, the benchmark is calculating the allocation of converting slice to map as well. Understood the problem
    – zaRRoc
    Nov 22 at 7:50










  • The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well . Some runtime behavior, not sure though. See pool.Buffer.
    – mh-cbon
    Nov 22 at 10:32















up vote
-2
down vote

favorite












I discovered very strange behaviour with go maps recently. The use case is to create a group of integers and have O(1) check for IsMember(id int).



The current implementation is :



func convertToMap(v int64) map[int64]void {
out := make(map[int64]void, len(v))
for _, i := range v {
out[i] = void{}
}
return out
}

type Group struct {
members map[int64]void
}

type void struct{}

func (g *Group) IsMember(input string) (ok bool) {
memberID, _ := strconv.ParseInt(input, 10, 64)
_, ok = g.members[memberID]
return
}


When i benchmark the IsMember method, until 6 million members, everything looks fine. But above that the map look up is taking 1 second for each lookup!!



The benchmark test:



func BenchmarkIsMember(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
g := &Group{}
g.members = convertToMap(benchmarkV)

for N := 0; N < b.N && N < sizeOfGroup; N++ {
g.IsMember(benchmarkKVString[N])
}
}

var benchmarkV, benchmarkKVString = func(size int) (int64, string{
v := make(int64, size)
s := make(string, size)
for i := range v {
val := rand.Int63()
v[i] = val
s[i] = strconv.FormatInt(val, 10)
}
return v, s
}(sizeOfGroup)


Benchmark numbers:



const sizeOfGroup  = 6000000
BenchmarkIsMember-8 2000000 568 ns/op 50 B/op 0 allocs/op

const sizeOfGroup = 6830000
BenchmarkIsMember-8 1 1051725455 ns/op 178767208 B/op 25 allocs/op


Anything above group size of 6.8 million gives the same result.



Can someone help me to explain why this is happening, and can anything be done to make this performant while still using maps?



Also, i dont understand why so much memory is being allocated? Even if the time taken is due to collision and then linked list traversal, there shouldn't be any mem allocation, is my thought process wrong?










share|improve this question
























  • Your code doesn't compile.
    – peterSO
    Nov 22 at 5:29






  • 1




    i dont understand why so much memory is being allocated. That s a subtle statement given that you have written make(map[int64]void, len(v)). see also play.golang.org/p/JEEI4qkfYyn
    – mh-cbon
    Nov 22 at 5:57










  • @mh-cbon if this was the case, then the benchmark for 6 million should have shown the same mem allocation as well. The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well
    – zaRRoc
    Nov 22 at 7:44










  • @mh-cbon nevermind, the benchmark is calculating the allocation of converting slice to map as well. Understood the problem
    – zaRRoc
    Nov 22 at 7:50










  • The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well . Some runtime behavior, not sure though. See pool.Buffer.
    – mh-cbon
    Nov 22 at 10:32













up vote
-2
down vote

favorite









up vote
-2
down vote

favorite











I discovered very strange behaviour with go maps recently. The use case is to create a group of integers and have O(1) check for IsMember(id int).



The current implementation is :



func convertToMap(v int64) map[int64]void {
out := make(map[int64]void, len(v))
for _, i := range v {
out[i] = void{}
}
return out
}

type Group struct {
members map[int64]void
}

type void struct{}

func (g *Group) IsMember(input string) (ok bool) {
memberID, _ := strconv.ParseInt(input, 10, 64)
_, ok = g.members[memberID]
return
}


When i benchmark the IsMember method, until 6 million members, everything looks fine. But above that the map look up is taking 1 second for each lookup!!



The benchmark test:



func BenchmarkIsMember(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
g := &Group{}
g.members = convertToMap(benchmarkV)

for N := 0; N < b.N && N < sizeOfGroup; N++ {
g.IsMember(benchmarkKVString[N])
}
}

var benchmarkV, benchmarkKVString = func(size int) (int64, string{
v := make(int64, size)
s := make(string, size)
for i := range v {
val := rand.Int63()
v[i] = val
s[i] = strconv.FormatInt(val, 10)
}
return v, s
}(sizeOfGroup)


Benchmark numbers:



const sizeOfGroup  = 6000000
BenchmarkIsMember-8 2000000 568 ns/op 50 B/op 0 allocs/op

const sizeOfGroup = 6830000
BenchmarkIsMember-8 1 1051725455 ns/op 178767208 B/op 25 allocs/op


Anything above group size of 6.8 million gives the same result.



Can someone help me to explain why this is happening, and can anything be done to make this performant while still using maps?



Also, i dont understand why so much memory is being allocated? Even if the time taken is due to collision and then linked list traversal, there shouldn't be any mem allocation, is my thought process wrong?










share|improve this question















I discovered very strange behaviour with go maps recently. The use case is to create a group of integers and have O(1) check for IsMember(id int).



The current implementation is :



func convertToMap(v int64) map[int64]void {
out := make(map[int64]void, len(v))
for _, i := range v {
out[i] = void{}
}
return out
}

type Group struct {
members map[int64]void
}

type void struct{}

func (g *Group) IsMember(input string) (ok bool) {
memberID, _ := strconv.ParseInt(input, 10, 64)
_, ok = g.members[memberID]
return
}


When i benchmark the IsMember method, until 6 million members, everything looks fine. But above that the map look up is taking 1 second for each lookup!!



The benchmark test:



func BenchmarkIsMember(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
g := &Group{}
g.members = convertToMap(benchmarkV)

for N := 0; N < b.N && N < sizeOfGroup; N++ {
g.IsMember(benchmarkKVString[N])
}
}

var benchmarkV, benchmarkKVString = func(size int) (int64, string{
v := make(int64, size)
s := make(string, size)
for i := range v {
val := rand.Int63()
v[i] = val
s[i] = strconv.FormatInt(val, 10)
}
return v, s
}(sizeOfGroup)


Benchmark numbers:



const sizeOfGroup  = 6000000
BenchmarkIsMember-8 2000000 568 ns/op 50 B/op 0 allocs/op

const sizeOfGroup = 6830000
BenchmarkIsMember-8 1 1051725455 ns/op 178767208 B/op 25 allocs/op


Anything above group size of 6.8 million gives the same result.



Can someone help me to explain why this is happening, and can anything be done to make this performant while still using maps?



Also, i dont understand why so much memory is being allocated? Even if the time taken is due to collision and then linked list traversal, there shouldn't be any mem allocation, is my thought process wrong?







go hashmap maps hash-collision






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Nov 22 at 8:08

























asked Nov 22 at 3:40









zaRRoc

458




458












  • Your code doesn't compile.
    – peterSO
    Nov 22 at 5:29






  • 1




    i dont understand why so much memory is being allocated. That s a subtle statement given that you have written make(map[int64]void, len(v)). see also play.golang.org/p/JEEI4qkfYyn
    – mh-cbon
    Nov 22 at 5:57










  • @mh-cbon if this was the case, then the benchmark for 6 million should have shown the same mem allocation as well. The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well
    – zaRRoc
    Nov 22 at 7:44










  • @mh-cbon nevermind, the benchmark is calculating the allocation of converting slice to map as well. Understood the problem
    – zaRRoc
    Nov 22 at 7:50










  • The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well . Some runtime behavior, not sure though. See pool.Buffer.
    – mh-cbon
    Nov 22 at 10:32


















  • Your code doesn't compile.
    – peterSO
    Nov 22 at 5:29






  • 1




    i dont understand why so much memory is being allocated. That s a subtle statement given that you have written make(map[int64]void, len(v)). see also play.golang.org/p/JEEI4qkfYyn
    – mh-cbon
    Nov 22 at 5:57










  • @mh-cbon if this was the case, then the benchmark for 6 million should have shown the same mem allocation as well. The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well
    – zaRRoc
    Nov 22 at 7:44










  • @mh-cbon nevermind, the benchmark is calculating the allocation of converting slice to map as well. Understood the problem
    – zaRRoc
    Nov 22 at 7:50










  • The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well . Some runtime behavior, not sure though. See pool.Buffer.
    – mh-cbon
    Nov 22 at 10:32
















Your code doesn't compile.
– peterSO
Nov 22 at 5:29




Your code doesn't compile.
– peterSO
Nov 22 at 5:29




1




1




i dont understand why so much memory is being allocated. That s a subtle statement given that you have written make(map[int64]void, len(v)). see also play.golang.org/p/JEEI4qkfYyn
– mh-cbon
Nov 22 at 5:57




i dont understand why so much memory is being allocated. That s a subtle statement given that you have written make(map[int64]void, len(v)). see also play.golang.org/p/JEEI4qkfYyn
– mh-cbon
Nov 22 at 5:57












@mh-cbon if this was the case, then the benchmark for 6 million should have shown the same mem allocation as well. The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well
– zaRRoc
Nov 22 at 7:44




@mh-cbon if this was the case, then the benchmark for 6 million should have shown the same mem allocation as well. The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well
– zaRRoc
Nov 22 at 7:44












@mh-cbon nevermind, the benchmark is calculating the allocation of converting slice to map as well. Understood the problem
– zaRRoc
Nov 22 at 7:50




@mh-cbon nevermind, the benchmark is calculating the allocation of converting slice to map as well. Understood the problem
– zaRRoc
Nov 22 at 7:50












The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well . Some runtime behavior, not sure though. See pool.Buffer.
– mh-cbon
Nov 22 at 10:32




The benchmark for 6 million and 6.8 million has a wide contrast in both time to lookup and mem allocation as well . Some runtime behavior, not sure though. See pool.Buffer.
– mh-cbon
Nov 22 at 10:32












1 Answer
1






active

oldest

votes

















up vote
1
down vote



accepted










No need to measure extra allocation for converting slice to map because we just want to measure the lookup operation.



I've slightly modify the benchmark:



func BenchmarkIsMember(b *testing.B) {
fn := func(size int) (int64, string) {
v := make(int64, size)
s := make(string, size)

for i := range v {
val := rand.Int63()
v[i] = val
s[i] = strconv.FormatInt(val, 10)
}

return v, s
}

for _, size := range int{
6000000,
6800000,
6830000,
60000000,
} {
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
var benchmarkV, benchmarkKVString = fn(size)

g := &deltaGroup{}
g.members = convertToMap(benchmarkV)

b.ReportAllocs()
b.ResetTimer()

for N := 0; N < b.N && N < size; N++ {
g.IsMember(benchmarkKVString[N])
}
})
}
}


And got the following results:



go test ./... -bench=. -benchtime=10s -cpu=1
goos: linux
goarch: amd64
pkg: trash
BenchmarkIsMember/size=6000000 2000000000 0.55 ns/op 0 B/op 0 allocs/op
BenchmarkIsMember/size=6800000 1000000000 1.27 ns/op 0 B/op 0 allocs/op
BenchmarkIsMember/size=6830000 1000000000 1.23 ns/op 0 B/op 0 allocs/op
BenchmarkIsMember/size=60000000 100000000 136 ns/op 0 B/op 0 allocs/op
PASS
ok trash 167.578s


Degradation isn't so significant as in your example.






share|improve this answer























    Your Answer






    StackExchange.ifUsing("editor", function () {
    StackExchange.using("externalEditor", function () {
    StackExchange.using("snippets", function () {
    StackExchange.snippets.init();
    });
    });
    }, "code-snippets");

    StackExchange.ready(function() {
    var channelOptions = {
    tags: "".split(" "),
    id: "1"
    };
    initTagRenderer("".split(" "), "".split(" "), channelOptions);

    StackExchange.using("externalEditor", function() {
    // Have to fire editor after snippets, if snippets enabled
    if (StackExchange.settings.snippets.snippetsEnabled) {
    StackExchange.using("snippets", function() {
    createEditor();
    });
    }
    else {
    createEditor();
    }
    });

    function createEditor() {
    StackExchange.prepareEditor({
    heartbeatType: 'answer',
    convertImagesToLinks: true,
    noModals: true,
    showLowRepImageUploadWarning: true,
    reputationToPostImages: 10,
    bindNavPrevention: true,
    postfix: "",
    imageUploader: {
    brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
    contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
    allowUrls: true
    },
    onDemand: true,
    discardSelector: ".discard-answer"
    ,immediatelyShowMarkdownHelp:true
    });


    }
    });














    draft saved

    draft discarded


















    StackExchange.ready(
    function () {
    StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53423553%2fgo-maps-non-performant-for-large-number-of-keys%23new-answer', 'question_page');
    }
    );

    Post as a guest















    Required, but never shown

























    1 Answer
    1






    active

    oldest

    votes








    1 Answer
    1






    active

    oldest

    votes









    active

    oldest

    votes






    active

    oldest

    votes








    up vote
    1
    down vote



    accepted










    No need to measure extra allocation for converting slice to map because we just want to measure the lookup operation.



    I've slightly modify the benchmark:



    func BenchmarkIsMember(b *testing.B) {
    fn := func(size int) (int64, string) {
    v := make(int64, size)
    s := make(string, size)

    for i := range v {
    val := rand.Int63()
    v[i] = val
    s[i] = strconv.FormatInt(val, 10)
    }

    return v, s
    }

    for _, size := range int{
    6000000,
    6800000,
    6830000,
    60000000,
    } {
    b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
    var benchmarkV, benchmarkKVString = fn(size)

    g := &deltaGroup{}
    g.members = convertToMap(benchmarkV)

    b.ReportAllocs()
    b.ResetTimer()

    for N := 0; N < b.N && N < size; N++ {
    g.IsMember(benchmarkKVString[N])
    }
    })
    }
    }


    And got the following results:



    go test ./... -bench=. -benchtime=10s -cpu=1
    goos: linux
    goarch: amd64
    pkg: trash
    BenchmarkIsMember/size=6000000 2000000000 0.55 ns/op 0 B/op 0 allocs/op
    BenchmarkIsMember/size=6800000 1000000000 1.27 ns/op 0 B/op 0 allocs/op
    BenchmarkIsMember/size=6830000 1000000000 1.23 ns/op 0 B/op 0 allocs/op
    BenchmarkIsMember/size=60000000 100000000 136 ns/op 0 B/op 0 allocs/op
    PASS
    ok trash 167.578s


    Degradation isn't so significant as in your example.






    share|improve this answer



























      up vote
      1
      down vote



      accepted










      No need to measure extra allocation for converting slice to map because we just want to measure the lookup operation.



      I've slightly modify the benchmark:



      func BenchmarkIsMember(b *testing.B) {
      fn := func(size int) (int64, string) {
      v := make(int64, size)
      s := make(string, size)

      for i := range v {
      val := rand.Int63()
      v[i] = val
      s[i] = strconv.FormatInt(val, 10)
      }

      return v, s
      }

      for _, size := range int{
      6000000,
      6800000,
      6830000,
      60000000,
      } {
      b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
      var benchmarkV, benchmarkKVString = fn(size)

      g := &deltaGroup{}
      g.members = convertToMap(benchmarkV)

      b.ReportAllocs()
      b.ResetTimer()

      for N := 0; N < b.N && N < size; N++ {
      g.IsMember(benchmarkKVString[N])
      }
      })
      }
      }


      And got the following results:



      go test ./... -bench=. -benchtime=10s -cpu=1
      goos: linux
      goarch: amd64
      pkg: trash
      BenchmarkIsMember/size=6000000 2000000000 0.55 ns/op 0 B/op 0 allocs/op
      BenchmarkIsMember/size=6800000 1000000000 1.27 ns/op 0 B/op 0 allocs/op
      BenchmarkIsMember/size=6830000 1000000000 1.23 ns/op 0 B/op 0 allocs/op
      BenchmarkIsMember/size=60000000 100000000 136 ns/op 0 B/op 0 allocs/op
      PASS
      ok trash 167.578s


      Degradation isn't so significant as in your example.






      share|improve this answer

























        up vote
        1
        down vote



        accepted







        up vote
        1
        down vote



        accepted






        No need to measure extra allocation for converting slice to map because we just want to measure the lookup operation.



        I've slightly modify the benchmark:



        func BenchmarkIsMember(b *testing.B) {
        fn := func(size int) (int64, string) {
        v := make(int64, size)
        s := make(string, size)

        for i := range v {
        val := rand.Int63()
        v[i] = val
        s[i] = strconv.FormatInt(val, 10)
        }

        return v, s
        }

        for _, size := range int{
        6000000,
        6800000,
        6830000,
        60000000,
        } {
        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
        var benchmarkV, benchmarkKVString = fn(size)

        g := &deltaGroup{}
        g.members = convertToMap(benchmarkV)

        b.ReportAllocs()
        b.ResetTimer()

        for N := 0; N < b.N && N < size; N++ {
        g.IsMember(benchmarkKVString[N])
        }
        })
        }
        }


        And got the following results:



        go test ./... -bench=. -benchtime=10s -cpu=1
        goos: linux
        goarch: amd64
        pkg: trash
        BenchmarkIsMember/size=6000000 2000000000 0.55 ns/op 0 B/op 0 allocs/op
        BenchmarkIsMember/size=6800000 1000000000 1.27 ns/op 0 B/op 0 allocs/op
        BenchmarkIsMember/size=6830000 1000000000 1.23 ns/op 0 B/op 0 allocs/op
        BenchmarkIsMember/size=60000000 100000000 136 ns/op 0 B/op 0 allocs/op
        PASS
        ok trash 167.578s


        Degradation isn't so significant as in your example.






        share|improve this answer














        No need to measure extra allocation for converting slice to map because we just want to measure the lookup operation.



        I've slightly modify the benchmark:



        func BenchmarkIsMember(b *testing.B) {
        fn := func(size int) (int64, string) {
        v := make(int64, size)
        s := make(string, size)

        for i := range v {
        val := rand.Int63()
        v[i] = val
        s[i] = strconv.FormatInt(val, 10)
        }

        return v, s
        }

        for _, size := range int{
        6000000,
        6800000,
        6830000,
        60000000,
        } {
        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
        var benchmarkV, benchmarkKVString = fn(size)

        g := &deltaGroup{}
        g.members = convertToMap(benchmarkV)

        b.ReportAllocs()
        b.ResetTimer()

        for N := 0; N < b.N && N < size; N++ {
        g.IsMember(benchmarkKVString[N])
        }
        })
        }
        }


        And got the following results:



        go test ./... -bench=. -benchtime=10s -cpu=1
        goos: linux
        goarch: amd64
        pkg: trash
        BenchmarkIsMember/size=6000000 2000000000 0.55 ns/op 0 B/op 0 allocs/op
        BenchmarkIsMember/size=6800000 1000000000 1.27 ns/op 0 B/op 0 allocs/op
        BenchmarkIsMember/size=6830000 1000000000 1.23 ns/op 0 B/op 0 allocs/op
        BenchmarkIsMember/size=60000000 100000000 136 ns/op 0 B/op 0 allocs/op
        PASS
        ok trash 167.578s


        Degradation isn't so significant as in your example.







        share|improve this answer














        share|improve this answer



        share|improve this answer








        edited Nov 22 at 5:55

























        answered Nov 22 at 5:39









        dshil

        276110




        276110






























            draft saved

            draft discarded




















































            Thanks for contributing an answer to Stack Overflow!


            • Please be sure to answer the question. Provide details and share your research!

            But avoid



            • Asking for help, clarification, or responding to other answers.

            • Making statements based on opinion; back them up with references or personal experience.


            To learn more, see our tips on writing great answers.





            Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


            Please pay close attention to the following guidance:


            • Please be sure to answer the question. Provide details and share your research!

            But avoid



            • Asking for help, clarification, or responding to other answers.

            • Making statements based on opinion; back them up with references or personal experience.


            To learn more, see our tips on writing great answers.




            draft saved


            draft discarded














            StackExchange.ready(
            function () {
            StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53423553%2fgo-maps-non-performant-for-large-number-of-keys%23new-answer', 'question_page');
            }
            );

            Post as a guest















            Required, but never shown





















































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown

































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown







            Popular posts from this blog

            Berounka

            Sphinx de Gizeh

            Different font size/position of beamer's navigation symbols template's content depending on regular/plain...