Golang的append方法和STL的push_back方法在内存分配方面的区别

3

我比较了Go语言中的append函数和STL库中的vector.push_back函数,并发现它们采用了不同的内存分配策略,这让我感到困惑。以下是代码:

// CPP STL code
void getAlloc() {
    vector<double> arr;
    int s = 9999999; 
    int precap = arr.capacity();
    for (int i=0; i<s; i++) {
        if (precap < i) {
            arr.push_back(rand() % 12580 * 1.0);
            precap = arr.capacity();
            printf("%d  %p\n", precap, &arr[0]);
        } else {
            arr.push_back(rand() % 12580 * 1.0);
        }
    }
    printf("\n");
    return;
}


// Golang code    
func getAlloc() {
    arr := []float64{}
    size := 9999999
    pre := cap(arr)
    for i:=0; i<size; i++ {
        if pre < i {
            arr = append(arr, rand.NormFloat64())
            pre = cap(arr)
            log.Printf("%d %p\n", pre, &arr)
        } else {
            arr = append(arr, rand.NormFloat64())
        }
    }
    return;
}

但是内存地址不会随着大小扩展而改变,这真的让我感到困惑。 顺便说一下,在这两个实现(STL VS. Go)中,内存分配策略是不同的,我指的是扩展大小。有什么优缺点吗?这是上述代码的简化输出[大小和第一个元素地址]:

Golang                            CPP STL
2 0xc0800386c0                    2  004B19C0
4 0xc0800386c0                    4  004AE9B8
8 0xc0800386c0                    6  004B29E0
16 0xc0800386c0                   9  004B2A18
32 0xc0800386c0                   13  004B2A68
64 0xc0800386c0                   19  004B2AD8
128 0xc0800386c0                  28  004B29E0
256 0xc0800386c0                  42  004B2AC8
512 0xc0800386c0                  63  004B2C20
1024 0xc0800386c0                 94  004B2E20
1280 0xc0800386c0                 141  004B3118
1600 0xc0800386c0                 211  004B29E0
2000 0xc0800386c0                 316  004B3080
2500 0xc0800386c0                 474  004B3A68
3125 0xc0800386c0                 711  004B5FD0
3906 0xc0800386c0                 1066  004B7610
4882 0xc0800386c0                 1599  004B9768
6102 0xc0800386c0                 2398  004BC968
7627 0xc0800386c0                 3597  004C1460
9533 0xc0800386c0                 5395  004B5FD0
11916 0xc0800386c0                8092  004C0870
14895 0xc0800386c0                12138  004D0558
18618 0xc0800386c0                18207  004E80B0
23272 0xc0800386c0                27310  0050B9B0
29090 0xc0800386c0                40965  004B5FD0
36362 0xc0800386c0                61447  00590048
45452 0xc0800386c0                92170  003B0020
56815 0xc0800386c0                138255  00690020
71018 0xc0800386c0                207382  007A0020
....

更新:

请查看有关Golang内存分配策略的评论。

对于STL,策略取决于实现方式。请参阅此帖子以了解更多信息。

2个回答

2
你的Go和C++代码片段并不等价。在C++函数中,你打印的是向量中第一个元素的地址,而在Go示例中,你打印的是切片本身的地址。
像C++的std::vector一样,Go的切片是一个小型数据类型,它保存指向包含数据的底层数组的指针。该数据结构在整个函数中具有相同的地址。如果你想要切片中第一个元素的地址,你可以使用与C++相同的语法:&arr[0]

谢谢您的评论。顺便问一下,您能解释一下魔术增量分配的区别吗? - Jiaxiang
1
Go语言的策略似乎是将支持数组大小增加一倍,最多到1024个元素,然后以25%的增量递增。我不确定STL实现使用的是什么策略。 - James Henstridge

0
你得到的是切片头指针,而不是实际的后备数组。你可以把切片头想象成一个结构体,就像这样。
type SliceHeader struct {
    len,cap int
    backingArray unsafe.Pointer
}

当你追加元素并且后备数组被重新分配时,指针backingArray可能会被更改(不一定,但很可能)。然而,保存长度、容量和指向后备数组的指针的结构体的位置不会改变——它仍然在堆栈上,就在你声明它的地方。尝试打印&arr[0]而不是&arr,你应该看到更接近你期望的行为。

这与std::vector的行为基本相同。把一个切片看作更接近于一个vector而不是一个神奇的动态数组。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接