【问题标题】:Why is there a performance difference when I pass a slice argument as value or a pointer?当我将切片参数作为值或指针传递时,为什么会有性能差异?
【发布时间】:2021-03-01 12:36:39
【问题描述】:

我有以下代码:

func AddToSliceByValue(mySlice []int) {
    for idx := range mySlice {
        mySlice[idx]++
    }
}

func AddToSliceByPointer(mySlice *[]int) {
    for idx := range *mySlice {
        (*mySlice)[idx]++
    }
}

我的第一个想法是性能应该几乎相同,因为按值传递复制切片标头并按指针传递会迫使我取消引用指针,但我的基准测试显示了其他内容:

func BenchmarkAddByValue(b *testing.B) {
    mySlice := rand.Perm(1000)
    for n := 0; n < b.N; n++ {
        AddToSliceByValue(mySlice)
    }
}

func BenchmarkAddByPointer(b *testing.B) {
    mySlice := rand.Perm(1000)
    for n := 0; n < b.N; n++ {
        AddToSliceByPointer(&mySlice)
    }
}

BenchmarkAddByValue-12 1151256 1035 ns/op

BenchmarkAddByPointer-12 2145110 525 ns/op

谁能向我解释一下为什么性能差异如此之大?

我还添加了这两个函数的汇编代码。

传值汇编代码:

TEXT main.AddToSliceByValue(SB) /go_test/pointer/pointer_value.go
  pointer_value.go:4    0x1056f60       488b442410      MOVQ 0x10(SP), AX   
  pointer_value.go:4    0x1056f65       488b4c2408      MOVQ 0x8(SP), CX    
  pointer_value.go:4    0x1056f6a       31d2            XORL DX, DX     
  pointer_value.go:4    0x1056f6c       eb0e            JMP 0x1056f7c       
  pointer_value.go:5    0x1056f6e       488b1cd1        MOVQ 0(CX)(DX*8), BX    
  pointer_value.go:5    0x1056f72       48ffc3          INCQ BX         
  pointer_value.go:5    0x1056f75       48891cd1        MOVQ BX, 0(CX)(DX*8)    
  pointer_value.go:4    0x1056f79       48ffc2          INCQ DX         
  pointer_value.go:4    0x1056f7c       4839c2          CMPQ AX, DX     
  pointer_value.go:4    0x1056f7f       7ced            JL 0x1056f6e        
  pointer_value.go:4    0x1056f81       c3          RET         
  :-1           0x1056f82       cc          INT $0x3        
  :-1           0x1056f83       cc          INT $0x3        
  :-1           0x1056f84       cc          INT $0x3        
  :-1           0x1056f85       cc          INT $0x3        
  :-1           0x1056f86       cc          INT $0x3        
  :-1           0x1056f87       cc          INT $0x3        
  :-1           0x1056f88       cc          INT $0x3        
  :-1           0x1056f89       cc          INT $0x3        
  :-1           0x1056f8a       cc          INT $0x3        
  :-1           0x1056f8b       cc          INT $0x3        
  :-1           0x1056f8c       cc          INT $0x3        
  :-1           0x1056f8d       cc          INT $0x3        
  :-1           0x1056f8e       cc          INT $0x3        
  :-1           0x1056f8f       cc          INT $0x3        

TEXT main.main(SB) /go_test/pointer/pointer_value.go
  pointer_value.go:9    0x1056f90       65488b0c2530000000  MOVQ GS:0x30, CX            
  pointer_value.go:9    0x1056f99       483b6110        CMPQ 0x10(CX), SP           
  pointer_value.go:9    0x1056f9d       0f86a8000000        JBE 0x105704b               
  pointer_value.go:9    0x1056fa3       4883ec70        SUBQ $0x70, SP              
  pointer_value.go:9    0x1056fa7       48896c2468      MOVQ BP, 0x68(SP)           
  pointer_value.go:9    0x1056fac       488d6c2468      LEAQ 0x68(SP), BP           
  pointer_value.go:11   0x1056fb1       488d7c2418      LEAQ 0x18(SP), DI           
  pointer_value.go:11   0x1056fb6       0f57c0          XORPS X0, X0                
  pointer_value.go:11   0x1056fb9       488d7fd0        LEAQ -0x30(DI), DI          
  pointer_value.go:11   0x1056fbd       48896c24f0      MOVQ BP, -0x10(SP)          
  pointer_value.go:11   0x1056fc2       488d6c24f0      LEAQ -0x10(SP), BP          
  pointer_value.go:11   0x1056fc7       e849c6ffff      CALL 0x1053615              
  pointer_value.go:11   0x1056fcc       488b6d00        MOVQ 0(BP), BP              
  pointer_value.go:11   0x1056fd0       48c744242001000000  MOVQ $0x1, 0x20(SP)         
  pointer_value.go:11   0x1056fd9       48c744242802000000  MOVQ $0x2, 0x28(SP)         
  pointer_value.go:11   0x1056fe2       48c744243003000000  MOVQ $0x3, 0x30(SP)         
  pointer_value.go:11   0x1056feb       48c744243804000000  MOVQ $0x4, 0x38(SP)         
  pointer_value.go:11   0x1056ff4       48c744244005000000  MOVQ $0x5, 0x40(SP)         
  pointer_value.go:11   0x1056ffd       48c744244806000000  MOVQ $0x6, 0x48(SP)         
  pointer_value.go:11   0x1057006       48c744245007000000  MOVQ $0x7, 0x50(SP)         
  pointer_value.go:11   0x105700f       48c744245808000000  MOVQ $0x8, 0x58(SP)         
  pointer_value.go:11   0x1057018       48c744246009000000  MOVQ $0x9, 0x60(SP)         
  pointer_value.go:12   0x1057021       488d442418      LEAQ 0x18(SP), AX           
  pointer_value.go:12   0x1057026       48890424        MOVQ AX, 0(SP)              
  pointer_value.go:12   0x105702a       48c74424080a000000  MOVQ $0xa, 0x8(SP)          
  pointer_value.go:12   0x1057033       48c74424100a000000  MOVQ $0xa, 0x10(SP)         
  pointer_value.go:12   0x105703c       e81fffffff      CALL main.AddToSliceByValue(SB)     
  pointer_value.go:13   0x1057041       488b6c2468      MOVQ 0x68(SP), BP           
  pointer_value.go:13   0x1057046       4883c470        ADDQ $0x70, SP              
  pointer_value.go:13   0x105704a       c3          RET                 
  pointer_value.go:9    0x105704b       e8909cffff      CALL runtime.morestack_noctxt(SB)   
  pointer_value.go:9    0x1057050       e93bffffff      JMP main.main(SB)           

指针传递的汇编代码:

TEXT main.AddToSliceByPointer(SB) /go_test/pointer/pointer_ref.go
  pointer_ref.go:3  0x1056f60       4883ec18        SUBQ $0x18, SP          
  pointer_ref.go:3  0x1056f64       48896c2410      MOVQ BP, 0x10(SP)       
  pointer_ref.go:3  0x1056f69       488d6c2410      LEAQ 0x10(SP), BP       
  pointer_ref.go:4  0x1056f6e       488b542420      MOVQ 0x20(SP), DX       
  pointer_ref.go:4  0x1056f73       488b5a08        MOVQ 0x8(DX), BX        
  pointer_ref.go:4  0x1056f77       31c0            XORL AX, AX         
  pointer_ref.go:4  0x1056f79       eb0e            JMP 0x1056f89           
  pointer_ref.go:5  0x1056f7b       488b3cc6        MOVQ 0(SI)(AX*8), DI        
  pointer_ref.go:5  0x1056f7f       48ffc7          INCQ DI             
  pointer_ref.go:5  0x1056f82       48893cc6        MOVQ DI, 0(SI)(AX*8)        
  pointer_ref.go:4  0x1056f86       48ffc0          INCQ AX             
  pointer_ref.go:4  0x1056f89       4839d8          CMPQ BX, AX         
  pointer_ref.go:4  0x1056f8c       7d0e            JGE 0x1056f9c           
  pointer_ref.go:5  0x1056f8e       488b4a08        MOVQ 0x8(DX), CX        
  pointer_ref.go:5  0x1056f92       488b32          MOVQ 0(DX), SI          
  pointer_ref.go:5  0x1056f95       4839c8          CMPQ CX, AX         
  pointer_ref.go:5  0x1056f98       72e1            JB 0x1056f7b            
  pointer_ref.go:5  0x1056f9a       eb0a            JMP 0x1056fa6           
  pointer_ref.go:4  0x1056f9c       488b6c2410      MOVQ 0x10(SP), BP       
  pointer_ref.go:4  0x1056fa1       4883c418        ADDQ $0x18, SP          
  pointer_ref.go:4  0x1056fa5       c3          RET             
  pointer_ref.go:5  0x1056fa6       e8b5c4ffff      CALL runtime.panicIndex(SB) 
  pointer_ref.go:5  0x1056fab       90          NOPL                
  :-1           0x1056fac       cc          INT $0x3            
  :-1           0x1056fad       cc          INT $0x3            
  :-1           0x1056fae       cc          INT $0x3            
  :-1           0x1056faf       cc          INT $0x3            

TEXT main.main(SB) /go_test/pointer/pointer_ref.go
  pointer_ref.go:9  0x1056fb0       65488b0c2530000000  MOVQ GS:0x30, CX            
  pointer_ref.go:9  0x1056fb9       483b6110        CMPQ 0x10(CX), SP           
  pointer_ref.go:9  0x1056fbd       0f86b2000000        JBE 0x1057075               
  pointer_ref.go:9  0x1056fc3       4883ec78        SUBQ $0x78, SP              
  pointer_ref.go:9  0x1056fc7       48896c2470      MOVQ BP, 0x70(SP)           
  pointer_ref.go:9  0x1056fcc       488d6c2470      LEAQ 0x70(SP), BP           
  pointer_ref.go:11 0x1056fd1       488d7c2408      LEAQ 0x8(SP), DI            
  pointer_ref.go:11 0x1056fd6       0f57c0          XORPS X0, X0                
  pointer_ref.go:11 0x1056fd9       488d7fd0        LEAQ -0x30(DI), DI          
  pointer_ref.go:11 0x1056fdd       48896c24f0      MOVQ BP, -0x10(SP)          
  pointer_ref.go:11 0x1056fe2       488d6c24f0      LEAQ -0x10(SP), BP          
  pointer_ref.go:11 0x1056fe7       e829c6ffff      CALL 0x1053615              
  pointer_ref.go:11 0x1056fec       488b6d00        MOVQ 0(BP), BP              
  pointer_ref.go:11 0x1056ff0       48c744241001000000  MOVQ $0x1, 0x10(SP)         
  pointer_ref.go:11 0x1056ff9       48c744241802000000  MOVQ $0x2, 0x18(SP)         
  pointer_ref.go:11 0x1057002       48c744242003000000  MOVQ $0x3, 0x20(SP)         
  pointer_ref.go:11 0x105700b       48c744242804000000  MOVQ $0x4, 0x28(SP)         
  pointer_ref.go:11 0x1057014       48c744243005000000  MOVQ $0x5, 0x30(SP)         
  pointer_ref.go:11 0x105701d       48c744243806000000  MOVQ $0x6, 0x38(SP)         
  pointer_ref.go:11 0x1057026       48c744244007000000  MOVQ $0x7, 0x40(SP)         
  pointer_ref.go:11 0x105702f       48c744244808000000  MOVQ $0x8, 0x48(SP)         
  pointer_ref.go:11 0x1057038       48c744245009000000  MOVQ $0x9, 0x50(SP)         
  pointer_ref.go:11 0x1057041       488d442408      LEAQ 0x8(SP), AX            
  pointer_ref.go:11 0x1057046       4889442458      MOVQ AX, 0x58(SP)           
  pointer_ref.go:11 0x105704b       48c74424600a000000  MOVQ $0xa, 0x60(SP)         
  pointer_ref.go:11 0x1057054       48c74424680a000000  MOVQ $0xa, 0x68(SP)         
  pointer_ref.go:12 0x105705d       488d442458      LEAQ 0x58(SP), AX           
  pointer_ref.go:12 0x1057062       48890424        MOVQ AX, 0(SP)              
  pointer_ref.go:12 0x1057066       e8f5feffff      CALL main.AddToSliceByPointer(SB)   
  pointer_ref.go:13 0x105706b       488b6c2470      MOVQ 0x70(SP), BP           
  pointer_ref.go:13 0x1057070       4883c478        ADDQ $0x78, SP              
  pointer_ref.go:13 0x1057074       c3          RET                 
  pointer_ref.go:9  0x1057075       e8669cffff      CALL runtime.morestack_noctxt(SB)   
  pointer_ref.go:9  0x105707a       e931ffffff      JMP main.main(SB)           

        

【问题讨论】:

  • 可能不同的编译器优化。检查生成的汇编代码。
  • Microbenchmark 是谎言的另一个词。另请注意,您的微基准测试包括 rand.Perm 的时间。如果您对编译器生成不同代码的原因和方式的详细信息感兴趣:在 golang-nuts 上提问。
  • 嘿沃尔克,我想如果我把东西放在基准循环之外,它不会影响时间。
  • 在您的情况下,重置计时器并没有什么意义,因为无论如何 rand.Perm 会以恒定的方式大致影响您的结果,因为我们正在查看两个版本的差异,而不是它们的差异确切的时间。

标签: performance go pointers slice


【解决方案1】:

我无法重现您的基准...

package main_test

import (
    "math/rand"
    "testing"
)

func AddToSliceByValue(mySlice []int) {
    for idx := range mySlice {
        mySlice[idx]++
    }
}

func AddToSliceByPointer(mySlice *[]int) {
    for idx := range *mySlice {
        (*mySlice)[idx]++
    }
}

func BenchmarkAddByValue(b *testing.B) {
    mySlice := rand.Perm(1000)
    for n := 0; n < b.N; n++ {
        AddToSliceByValue(mySlice)
    }
}

func BenchmarkAddByPointer(b *testing.B) {
    mySlice := rand.Perm(1000)
    for n := 0; n < b.N; n++ {
        AddToSliceByPointer(&mySlice)
    }
}
$ go test -bench=. -benchmem -count=4
goos: linux
goarch: amd64
pkg: test/bencslice
BenchmarkAddByValue-4        3010280           385 ns/op           0 B/op          0 allocs/op
BenchmarkAddByValue-4        3118990           385 ns/op           0 B/op          0 allocs/op
BenchmarkAddByValue-4        3117450           384 ns/op           0 B/op          0 allocs/op
BenchmarkAddByValue-4        3109251           386 ns/op           0 B/op          0 allocs/op
BenchmarkAddByPointer-4      2012487           610 ns/op           0 B/op          0 allocs/op
BenchmarkAddByPointer-4      2009690           594 ns/op           0 B/op          0 allocs/op
BenchmarkAddByPointer-4      2009222           594 ns/op           0 B/op          0 allocs/op
BenchmarkAddByPointer-4      1850820           596 ns/op           0 B/op          0 allocs/op
PASS
ok      test/bencslice  13.476s
$ go version
go version go1.15.2 linux/amd64

无论如何,行为可能取决于许多因素,首先是运行时的版本。只要您可以测试、重现和监控,了解 intrinsec 就没什么意义。

【讨论】:

    【解决方案2】:

    我发现我的方差太高了:

    AddByValue-12    5.41µs ±15%
    AddByPointer-12  5.30µs ± 4%
    

    通过go test -benchmem -count 5 -benchtime=1000000x -bench=. ./...,我能够减少测试结果的差异,并且可以确认我的第一个假设,即结果应该大致相等:

    AddByValue-12    5.04µs ± 1%
    AddByPointer-12  5.17µs ± 1%
    

    根据 cmets 的说法,差异较大的主要原因是我没有在基准设置后重置计时器。

    通过以下代码和较短的工作时间,我还减少了差异:

    func BenchmarkAddByValue(b *testing.B) {
        mySlice := rand.Perm(10000)
        b.ResetTimer()
        for n := 0; n < b.N; n++ {
            AddToSliceByValue(mySlice)
        }
    }
    
    func BenchmarkAddByPointer(b *testing.B) {
        mySlice := rand.Perm(10000)
        b.ResetTimer()
        for n := 0; n < b.N; n++ {
            AddToSliceByPointer(&mySlice)
        }
    }
    

    结果:

    AddByValue-12    5.03µs ± 0%
    AddByPointer-12  5.17µs ± 1%
    

    非常感谢您的帮助!

    【讨论】:

      【解决方案3】:

      这是“低级”语言的普遍问题。当您按值传递时,这意味着您实际上是在复制数据。以下是它的工作原理。

      当你通过引用传递时:

      • 引用的副本被创建并传递给方法(引用很可能是 8 个字节,所以这很快)
      • 你读出了引用后面的数据(这也很快,因为引用很可能在 CPU 缓存中)

      传值情况下:

      • 分配内存块来存储你传入的数据(慢)
      • 数据被复制到新分配的内存块中(可能快,也可能慢)
      • 然后通过引用访问您的数据(可能很慢,也可能很快,具体取决于数据是否在缓存中)

      【讨论】:

      • 接收切片的函数获取切片标头(len、cap、指向数据的指针)的副本,这也应该很快。
      • 这里唯一通过值传递的是切片,而不是切片引用的值。 Demo.
      猜你喜欢
      • 2016-10-27
      • 1970-01-01
      • 2021-01-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多