【问题标题】:Vector performance in Go and C++Go 和 C++ 中的向量性能
【发布时间】:2014-09-26 07:49:59
【问题描述】:

请考虑 GO 和 C++11 中的这两个 sn-ps。在 C++ 中,std::vector 是一个具有分期 O(1) 插入操作的加倍数组。如何在 GO 中达到同样的性能?问题是这个 GO 代码在我的硬件上慢了大约 3 倍。运行多次。

编译:

  • go build vec.go(去版本go1.2.1 linux/amd64)
  • g++ -O2 -std=gnu++11 -o vec vec.cc (g++ (Ubuntu 4.8.2-19ubuntu1) 4.8.2)

GO 版本 (vec.go):

package main

type X struct {
    x int32
    y float64
}

const N int = 80000000

func main() {
    x := X{123, 2.64}
    s := make([]X, 1)
    for i := 0; i < N; i++ {
        s = append(s, x)
    }
}

C++11 版本(vec.cc):

#include <vector>

const int N = 80000000;

struct X {
        int x;
        double y;
};

int main(void)
{
        X x{123, 2.64};
        std::vector<X> s(1);
        for (int i = 0; i < N; ++i) {
                s.push_back(x);
        }
}

【问题讨论】:

  • 我不懂 Go,但如果你关心性能,你的 C++ 代码应该在循环之前调用 reserve
  • @jalf 不,我希望基准测试使用大小为 1 的相同初始数组。C++ 版本非常快。但是 Go 可能没有使用双数组算法。
  • 当然,但是比较低效的代码和低效的代码并没有告诉你什么有用。这就像在没有启用优化的情况下进行编译。如果你想比较哪个更快,两个变体都应该写得很快。
  • @jalf 更改保留大小不会改变任何内容。您认为什么是低效的?
  • 预先保留尺寸应该改变性能。它避免了重新分配内存和复制对象。这是摊销 O(1) 和 实际 O(1) 复杂度之间的差异。

标签: c++ performance c++11 vector go


【解决方案1】:

如果你事先知道元素的数量,你可以预先分配它:

s := make([]X, 0, N)
for i := 0; i < N; i++ {
    s = append(s, x)
}

同样使用 Go 1.3,编译器做了一些优化。

为了更好的矢量化,试试gccgo

【讨论】:

  • 谢谢,但不,N 仅用于测试目的。让我们假设您只追加和追加并且不知道它会持续多长时间的一般设置。
  • 你尝试过 go1.3 和 gccgo 吗?
  • 是的,go1.3 速度差不多,gccgo-4.9 再慢 3 倍!
  • 我只是好奇,你在 gccgo 中使用了哪些标志?如果你用过。
【解决方案2】:

Go 的规范对 append() 没有任何特殊的复杂性要求,但实际上它也是在摊销常数时间内实现的,如对 this question 的回答中所述。

当前实现的工作原理如下:对于小于 1024 的数组大小,它会根据需要加倍,而大于 1024 的数组大小会增加到原始大小的 1.25 倍。增加 1.25 倍仍然是摊销常数时间,但与总是翻倍的实现相比,它具有施加更高摊销常数因子的效果。但是 1.25 倍总体上浪费的内存更少。

如果您只获得了几次不同的性能行为(即使 N 非常大),那么您会看到不同的常数因素在起作用。我自己注意到gc 编译器生成的机器代码比gccgo 生成的机器代码效率更高。

要自己验证 Go 是否在摊销的常数时间内运行,请尝试绘制针对几个不同 N 值运行算法所需的时间。

【讨论】:

  • 终于!很好的答案。似乎大多数std::vector 实现每次都翻倍,所以1.25x 因子参数似乎很完美。谢谢。
  • @eeq:大多数vector 实现(无论如何我都看过)使用固定因子,但我最近看到的所有实现都使用了 1.5 倍而不是加倍。
  • “浪费更少的内存”——更具体地说——在现代操作系统/商用桌面/服务器硬件上,它浪费更少的 虚拟地址空间,这几乎可以免费分配给第一个64 位程序中的时间,但 32 位程序的潜在问题。即使对于 64 位应用程序,当虚拟地址空间已经由 RAM 支持作为早期动态内存使用的副作用时,它仍然可能是一个问题 - 即使当前动态内存使用超过峰值 size() 的尾随容量最终可能会被推送到交换内存请求没有触及它。
  • @TonyD 某些操作系统(如 Linux)默认不会过度使用内存。浪费虚拟地址空间可能是也可能不是问题。
  • @FUZxxl:Linux 默认不使用heuristic overcommit handling 吗?无论如何,“可能是也可能不是问题”是我在上面试图强调的全部内容,并有一点理由/见解来支持它。
【解决方案3】:

我已经回答了您的计算复杂性问题:append complexity。它是摊销的常数时间。

我的基准测试结果。

$ rm vec
$ cat vec.cc
#include <vector>

const int N = 80000000;

struct X {
        int x;
        double y;
};

int main(void)
{
        X x{123, 2.64};
        std::vector<X> s(1);
        for (int i = 0; i < N; ++i) {
                s.push_back(x);
        }
}
$ g++ -O2 -std=gnu++11 -o vec vec.cc
$ time ./vec
real    0m1.360s
user    0m0.536s
sys 0m0.816s
$ rm vec
$ cat vec.go
package main

type X struct {
    x int32
    y float64
}

const N int = 80000000

func main() {
    x := X{123, 2.64}
    s := make([]X, 1)
    for i := 0; i < N; i++ {
        s = append(s, x)
    }
}
$ go version
go version devel +6b696a34e0af Sun Aug 03 15:14:59 2014 -0700 linux/amd64
$ go build vec.go
$ time ./vec
real    0m2.590s
user    0m1.192s
sys 0m1.388s
$ 

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-06-06
    • 1970-01-01
    • 1970-01-01
    • 2014-05-07
    • 1970-01-01
    • 2021-06-14
    相关资源
    最近更新 更多