doulu7921 2015-01-05 22:14
浏览 32

可变参数函数导致Go中不必要的堆分配

I'm currently working on some performance sensitive code in Go. At one point I have a particularly tight inner loop which does three things in succession:

  1. Obtain several pointers to data. In the event of a rare error, one or more of these pointers might be nil.

  2. Check whether this error has occurred, and log an error if it has.

  3. Do work with the data stored in the pointers.

Shown below is a toy program with the same structure (although the pointers can never actually be nil).

package main

import (
    "math/rand"
    "fmt"
)

const BigScaryNumber = 1<<25

func DoWork() {
    sum := 0
    for i := 0; i < BigScaryNumber; i++ {
        // Generate pointers.
        n1, n2 := rand.Intn(20), rand.Intn(20)
        ptr1, ptr2 := &n1, &n2

        // Check if pointers are nil.
        if ptr1 == nil || ptr2 == nil {
            fmt.Printf("Pointers %v %v contain a nil.
", ptr1, ptr2)
            break
        }

        // Do work with pointer contents.
        sum += *ptr1 + *ptr2
    }
}

func main() {
    DoWork()
}

When I run this on my machine, I get the following:

$ go build alloc.go && time ./alloc 

real    0m5.466s
user    0m5.458s
sys     0m0.015s

However, if I remove the print statement, I get the following:

$ go build alloc_no_print.go && time ./alloc_no_print

real    0m4.070s
user    0m4.063s
sys     0m0.008s

Since the print statement is never actually called, I investigated whether the print statement was somehow causing the pointers to be allocated on the heap instead of the stack. Running the compiler with the -m flag on the original program gives:

$ go build -gcflags=-m alloc.go
# command-line-arguments
./alloc.go:14: moved to heap: n1
./alloc.go:15: &n1 escapes to heap
./alloc.go:14: moved to heap: n2
./alloc.go:15: &n2 escapes to heap
./alloc.go:19: DoWork ... argument does not escape

while doing this on a print statement-less program gives

$ go build -gcflags=-m alloc_no_print.go
# command-line-arguments
./alloc_no_print.go:14: DoWork &n1 does not escape
./alloc_no_print.go:14: DoWork &n2 does not escape

confirming that even an unused fmt.Printf() is causing heap allocations which have a very real effect on performance. I can get the same behavior by replacing fmt.Printf() with a variadic function which does nothing and takes *ints as parameters instead of interface{}s:

func VarArgsError(ptrs ...*int) {
    panic("An error has occurred.")
}

I think this behavior is because Go allocates pointers on the heap whenever they are placed in a slice (although I'm not sure that this is the actual behavior of the escape analysis routines, I don't see how it would safely be able to do otherwise).

There are two purposes to this question: first, I want to know if my analysis of the situation is correct, since I don't really understand how Go's escape analysis works. And second, I wanted suggestions for maintaining the behavior of the original program without causing unneeded allocations. My best guess is to wrap a Copy() function around the pointers prior to passing them into the print statement:

fmt.Printf("Pointers %v %v contain a nil.", Copy(ptr1), Copy(ptr2))

where Copy() is defined as

func Copy(ptr *int) *int {
    if ptr == nil {
        return nil
    } else {
        n := *ptr
        return &n
    }
}

While this gives me the same performance as the no print statement case, it's weird and not the sort of thing I want to rewrite for every variable type and then wrap around all of my error logging code.

  • 写回答

1条回答 默认 最新

  • dscpg80066 2015-01-13 15:08
    关注

    From Go FAQ,

    In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

    When the pointers are passed to a function, I think it fails the second part of escape analysis. For example, the function may assign the pointer to a global variable in its package which lives longer than the current stack. I don't think the current compiler does such deep escape analysis.

    One way to avoid the cost of allocation would be to move the allocation outside the loop and reassign the value to allocated memory inside the loop.

    func DoWork() {
        sum := 0
        n1, n2 := new(int), new(int)
    
        for i := 0; i < BigScaryNumber; i++ {
            *n1, *n2 = rand.Intn(20), rand.Intn(20)
            ptr1, ptr2 := n1, n2
    
            // Check if pointers are nil.
            if ptr1 == nil || ptr2 == nil {
                fmt.Printf("Pointers %v %v contain a nil.
    ", n1, n2)
                break
            }
    
            // Do work with pointer contents.
            sum += *ptr1 + *ptr2
        }
    }
    
    评论

报告相同问题?

悬赏问题

  • ¥15 BP神经网络控制倒立摆
  • ¥20 要这个数学建模编程的代码 并且能完整允许出来结果 完整的过程和数据的结果
  • ¥15 html5+css和javascript有人可以帮吗?图片要怎么插入代码里面啊
  • ¥30 Unity接入微信SDK 无法开启摄像头
  • ¥20 有偿 写代码 要用特定的软件anaconda 里的jvpyter 用python3写
  • ¥20 cad图纸,chx-3六轴码垛机器人
  • ¥15 移动摄像头专网需要解vlan
  • ¥20 access多表提取相同字段数据并合并
  • ¥20 基于MSP430f5529的MPU6050驱动,求出欧拉角
  • ¥20 Java-Oj-桌布的计算