非通用映射在Golang中的效率

huangapple go评论69阅读模式
英文:

Non-generic Map in Golang Efficiency

问题

我完全理解Go语言不支持泛型,而是鼓励用户在需要时创建自己的类型特定方法。

然而,我想知道是否有一种更高效的方法来创建一个特定的map函数,而不需要遍历整个列表并应用函数,或者这是其他支持泛型的语言在幕后做的事情。

示例:

func map(list []string, op func(string)string) []string {
  ouput := make([]string, len(list))
  for i, v := range list {
    output[i] = op(v)
  }
  return output
}

谢谢!

英文:

I completely understand that Go doesn't provide support for Generics, opting instead for users to create their own type-specific methods when needed.

However, I am wondering if there is a more efficient way to make a specific map function over a data structure that does not involve looping through the entire list and applying the function, or is this what other languages with generics support do behind the scenes.

Example:

func map(list []string, op func(string)string) []string {
  ouput := make([]string, len(list))
  for i, v := range list {
    output[i] = op(v)
  }
  return output
}

Thanks!

答案1

得分: 4

FYI,“map”是一个保留字,所以你不能像原文中使用小写的“map”来命名你的函数。

这可能是你能得到的最好的结果了。泛型并不能避免分配新的内存。

使用append()比在开始时将整个切片初始化为空字符串要稍微快一些。例如:

func Map(list []string, op func(string) string) []string {
   output := make([]string, 0, len(list))
   for _, v := range list {
      output = append(output, op(v))
   }
   return output
}

这样做可以使速度提高约10%。

更新:这只对非常短的切片有效。下面是一个更全面的基准测试--在较长的切片上使用append实际上更慢。我还尝试了并行化,但只有在更大的切片上才值得付出额外的开销。

代码:
https://gist.github.com/8250514

输出(测试名称末尾的数字是切片长度):

go test -bench=".*" -test.cpu=2
BenchmarkSliceMake10-2         5000000         473 ns/op
BenchmarkSliceMake100-2        500000          3637 ns/op
BenchmarkSliceMake1000-2       50000           43920 ns/op
BenchmarkSliceMake10000-2      5000            539743 ns/op
BenchmarkSliceAppend10-2       5000000         464 ns/op
BenchmarkSliceAppend100-2      500000          4303 ns/op
BenchmarkSliceAppend1000-2     50000           51172 ns/op
BenchmarkSliceAppend10000-2    5000            595650 ns/op
BenchmarkSlicePar10-2          500000          3784 ns/op
BenchmarkSlicePar100-2         200000          7940 ns/op
BenchmarkSlicePar1000-2        50000           50118 ns/op
BenchmarkSlicePar10000-2       5000            465540 ns/op
英文:

FYI, map is a reserved word, so you can't make your function exactly as written with lowercase map.

That is probably as good as you can get. Generics don't get you around allocating new memory.

It is slightly faster to use append() rather than initializing the whole slice to empty strings at the beginning. For example:

func Map(list []string, op func(string) string) []string {
   output := make([]string, 0, len(list))
   for _, v := range list {
      output = append(output, op(v))
   }
   return output
}

This gave me about a 10% increase in speed.

Update: That was only true for very short slices. Here's a more thorough benchmark--it was actually slower to use append on longer slices. I also tried parallelizing it, which was only worth the overhead on much bigger slices.

Code:
https://gist.github.com/8250514

Output (numbers at end of test names are slice lengths):

go test -bench=".*" -test.cpu=2
BenchmarkSliceMake10-2		 5000000	       473 ns/op
BenchmarkSliceMake100-2		  500000	      3637 ns/op
BenchmarkSliceMake1000-2	   50000	     43920 ns/op
BenchmarkSliceMake10000-2	    5000	    539743 ns/op
BenchmarkSliceAppend10-2	 5000000	       464 ns/op
BenchmarkSliceAppend100-2	  500000	      4303 ns/op
BenchmarkSliceAppend1000-2	   50000	     51172 ns/op
BenchmarkSliceAppend10000-2	    5000	    595650 ns/op
BenchmarkSlicePar10-2		  500000	      3784 ns/op
BenchmarkSlicePar100-2		  200000	      7940 ns/op
BenchmarkSlicePar1000-2		   50000	     50118 ns/op
BenchmarkSlicePar10000-2	    5000	    465540 ns/op

答案2

得分: 3

是的,通常这正是泛型用于的情况。如果你愿意,可以使用反射在Go中编写此类函数,尽管它们会慢得多。例如,可以参考我的github.com/synful/illegal/generics包,该包使用反射来实现经典的泛型函数。我实际上还没有对这些进行基准测试,但如果它们与你提供的特定类型实现的性能相差太大,我会非常惊讶。

编辑:只是为了好玩,我复制了Wes Freeman的第一个测试,并在基于反射的Map上运行了它。这是在一台性能相对较低的服务器上运行的,但结果说明了一切(慢速性能相对于Wes的结果)。

BenchmarkSliceMake10-2    200000         14091 ns/op [慢30.0倍]
BenchmarkSliceMake100-2    10000        112137 ns/op [慢30.83倍]
BenchmarkSliceMake1000-2    2000       1177498 ns/op [慢26.810倍]
BenchmarkSliceMake10000-2    100      11513085 ns/op [慢21.3307倍]

注意:我特别使用了这个测试,因为我的实现预先分配了切片。

英文:

Yes, normally this is exactly the sort of thing that generics is used for. If you want, such functions can still be written in Go using reflection, although they're much slower. See, for example, my github.com/synful/illegal/generics package, which uses reflection to implement classic generic functions. I haven't actually benchmarked these, although I'd be very surprised if they were anywhere close to the performance of the type-specific implementation that you provided.

EDIT: Just for kicks, I copied Wes Freeman's first test, and ran it on my reflection-based Map. This was run on a fairly under-powered server, but still. The results speak for themselves (slowness is measured against Wes' results).

BenchmarkSliceMake10-2    200000         14091 ns/op [30.0x    slower]
BenchmarkSliceMake100-2    10000        112137 ns/op [30.83x   slower]
BenchmarkSliceMake1000-2    2000       1177498 ns/op [26.810x  slower]
BenchmarkSliceMake10000-2    100      11513085 ns/op [21.3307x slower]

Note: I used this test in particular because my implementation pre-allocates slices.

huangapple
  • 本文由 发表于 2014年1月4日 08:56:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/20915447.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定