go maps non-performant for large number of keys












-2















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 '18 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 '18 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 '18 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 '18 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 '18 at 10:32
















-2















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 '18 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 '18 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 '18 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 '18 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 '18 at 10:32














-2












-2








-2








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 '18 at 8:08







zaRRoc

















asked Nov 22 '18 at 3:40









zaRRoczaRRoc

559




559













  • Your code doesn't compile.

    – peterSO
    Nov 22 '18 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 '18 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 '18 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 '18 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 '18 at 10:32



















  • Your code doesn't compile.

    – peterSO
    Nov 22 '18 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 '18 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 '18 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 '18 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 '18 at 10:32

















Your code doesn't compile.

– peterSO
Nov 22 '18 at 5:29





Your code doesn't compile.

– peterSO
Nov 22 '18 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 '18 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 '18 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 '18 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 '18 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 '18 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 '18 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 '18 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 '18 at 10:32












1 Answer
1






active

oldest

votes


















1














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',
    autoActivateHeartbeat: false,
    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









    1














    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






























      1














      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




























        1












        1








        1







        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 '18 at 5:55

























        answered Nov 22 '18 at 5:39









        dshildshil

        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.




            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

            How to change which sound is reproduced for terminal bell?

            Title Spacing in Bjornstrup Chapter, Removing Chapter Number From Contents

            Can I use Tabulator js library in my java Spring + Thymeleaf project?