Golang因缓存未命中导致的执行速度变慢问题探讨

Golang因缓存未命中导致的执行速度变慢问题探讨 我开发了一个小型的选择排序代码,在Go程序中使用切片,在C++中使用向量。

注意:C++代码使用O2标志编译。 为什么要比较?只是为了了解Go能达到什么样的速度水平。

我发现Go需要10秒来完成排序,而C++向量仅需5秒(几乎是Go的一半)。 然后我尝试使用perf工具运行,以查看是否存在缓存问题,我发现了以下统计数据:

缓存未命中: 对于Go -> 15,397,360 对于C++ -> 1,808,180

我是否遗漏了某些Go编译器标志来优化代码? go build -o go_test selection_sort.go 是否有任何API可以预取缓冲区,例如GCC中的__builtin_prefetch?还是编译器会处理这个问题?

谢谢, ~Rohit


更多关于Golang因缓存未命中导致的执行速度变慢问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

你能提供一下代码吗?这样更容易帮助你。

更多关于Golang因缓存未命中导致的执行速度变慢问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Go 中是否有任何机制可以实现数据预取?例如,在 gcc 编译器中是 __builtin_prefetch(…) 或 _mm_prefetch(…)。

感谢,使用排序库来排序元素总是更好的选择。

但我选择了选择排序来比较Go与C的性能,原因只是想探索并了解Go的能力。这对我来说似乎很有趣。

恐怕你无法修改预取行为。Go语言在这方面非常受限。我认为这几行代码就是这种情况:

buf[i], buf[min] = buf[min], buf[i]

即使它被预取了,下一次操作也会使其失效。 顺便问一下,你为什么要自己实现排序算法?是sort.Sort太慢了吗?https://golang.org/pkg/sort/#Sort

我在C代码中也进行了尝试,在使用-O2标志时,其性能与Go相当。但是,当我将标志更改为-O3时,所需时间减少,与C++类似。随后,在C代码中使用优化标志时,我尝试使用了__builtin_prefetch代码,发现性能得到了提升。

这表明Go编译器没有添加预取指令,而这在当今世界中非常重要。 如果有任何类似的机制在Go语言中可用,请告知我。

oid sort_ints(int * ptr, int count) {
int temp, min;
clock_t start, stop;
double total;

start = clock();

for (int i = 0; i < count - 1; i++)
{
    min = i;
    __builtin_prefetch(&ptr[min], 1, 3);
    for (int j = min + 1; j < count; j++)
    {
        __builtin_prefetch(&ptr[j+1], 0, 0);
        if(ptr[j] < ptr[min]) {
            min = j;
            __builtin_prefetch(&ptr[min], 1, 2);
        }        
    }

    temp = ptr[min];
    ptr[min] = ptr[i];
    ptr[i] = temp;
}

stop = clock();
total = (double)(stop -start) / CLOCKS_PER_SEC;

printf("C Execution time : %f \n", total);

如果我有任何错误,请指正。

-------------- C++ 代码 ----------------------

void updateVector(vector &ptr, int count) {
for (int i = 0; i < count; i++)
{
ptr.push_back(count - i);
// cout << i << " -> " << ptr[i] << endl;
}

}

void selection_sort(vector &ptr) {

clock_t start, stop;
double total;
long nsec;
long sec;
int min = 0;

cout << "Vector size : " << ptr.size() << endl;

start = clock();

for (int i = 0; i < (ptr.size() - 1); i++)
{
	min = i;
	for (int j = min + 1; j < ptr.size(); j++) {
		if(ptr[j] < ptr[min]) {
			min = j;
		}
	}

	int temp = ptr[min];
	ptr[min] = ptr[i];
	ptr[i] = temp;
}

stop = clock();

total = (double)(stop - start) / CLOCKS_PER_SEC;

cout << "CPP Execution time : " << total << endl;

}

在 main 函数中,先调用 update 函数,然后再调用排序函数。

----------------------------------------- Go 代码 ---------------------------------------------------

func buildBuffer(count int) []int {
var buffer []int

buffer = make([]int, count)

for i := 0; i < count; i++ {
	buffer[i] = count - i
	//fmt.Printf("%d -> %d \n", i, buffer[i])
}

return buffer

}

func selection_sort(buf []int, count int) {
min := 0
t1 := time.Now()

for i := 0; i < count-1; i++ {
	min = i
	for j := i + 1; j < count; j++ {
		if buf[min] > buf[j] {
			min = j
		}
	}
	buf[i], buf[min] = buf[min], buf[i]
}

t2 := time.Now()
elapse := t2.Sub(t1)
fmt.Printf("go Execution time : %f \n", elapse.Seconds())

}

在Go中确实没有类似GCC __builtin_prefetch 的直接预取指令,但可以通过代码优化来改善缓存命中率。你的缓存未命中差异主要源于Go和C++内存模型的不同,以及编译器优化的差异。

Go的切片在内存中是连续分配的,但选择排序算法本身会导致较多的随机访问模式,这在Go中可能表现更差。以下是优化建议:

  1. 使用编译器优化标志
go build -gcflags="-N -l" -o go_test selection_sort.go  # 禁用优化(调试用)
go build -gcflags="-B" -o go_test selection_sort.go     # 禁用边界检查(谨慎使用)
  1. 优化内存访问模式
// 示例:改进局部性的选择排序实现
func selectionSort(arr []int) {
    n := len(arr)
    for i := 0; i < n-1; i++ {
        minIdx := i
        // 将内层循环的访问模式优化为顺序访问
        for j := i + 1; j < n; j++ {
            if arr[j] < arr[minIdx] {
                minIdx = j
            }
        }
        // 减少交换次数
        if minIdx != i {
            arr[i], arr[minIdx] = arr[minIdx], arr[i]
        }
    }
}
  1. 使用适当的数据结构
// 对于排序密集型任务,考虑使用sort包的内置函数
import "sort"
sort.Ints(arr)  // 使用高度优化的排序算法
  1. 调整运行时参数
// 在程序启动时设置GC和调度器参数
func init() {
    // 减少GC压力
    debug.SetGCPercent(100)
}
  1. 使用CPU亲和性(Linux):
import "golang.org/x/sys/unix"

func setCPUAffinity(cpu int) error {
    var mask unix.CPUSet
    mask.Set(cpu)
    return unix.SchedSetaffinity(0, &mask)
}

C++的O2优化包含自动向量化和更好的循环优化,而Go的编译器优化相对保守。对于性能关键代码,可以考虑:

  • 使用//go:noinline//go:nosplit指令控制函数内联
  • 通过sync.Pool重用对象减少分配
  • 使用unsafe.Pointer进行手动内存操作(需谨慎)

最终建议使用pprof进行性能分析:

go test -cpuprofile=cpu.prof -bench=.
go tool pprof cpu.prof

选择排序本身的时间复杂度为O(n²),对于大规模数据建议使用更高效的排序算法。Go的sort包实现了快速排序、堆排序和插入排序的混合算法,通常比手写选择排序快一个数量级。

回到顶部