Как достичь пиковых провалов

#go #parallel-processing #vectorization #gccgo

# #Вперед #параллельная обработка #векторизация #gccgo

Вопрос:

Я опытный программист на C , привыкший к низкоуровневой оптимизации, и я пытаюсь добиться производительности от Go.

Пока меня интересуют GFlop / s.

Я написал следующий код go:

 package main

import (
        "fmt"
        "time"
        "runtime"
        "sync"
)


func expm1(x float64) float64 {
        return ((((((((((((((15.0   x) * x   210.0) * x   2730.0) * x   32760.0) * x   360360.0) * x   3603600.0) * x   32432400.0) * x   259459200.0) * x   1816214400.0) * x   10897286400.0) * x   54486432000.0) * x   217945728000.0) *
x   653837184000.0) * x   1307674368000.0) * x * 7.6471637318198164759011319857881e-13;
}

func twelve(x float64) float64 {
        return expm1( expm1( expm1( expm1( expm1( expm1( expm1( expm1( expm1( expm1( expm1( expm1(x))))))))))));
}

func populate(data []float64, N int) {
        CPUCOUNT := runtime.NumCPU();
        var wg sync.WaitGroup
        var slice = N / CPUCOUNT;
        wg.Add(CPUCOUNT)
        defer wg.Wait()

        for i := 0; i < CPUCOUNT; i   {
                go func(ii int) {
                        for j := ii * slice; j < ii * slice   slice; j  = 1 {
                                data[j] = 0.1;
                        }
                        defer wg.Done();
                }(i);
        }
}

func apply(data []float64, N int) {
        CPUCOUNT := runtime.NumCPU();
        var wg sync.WaitGroup
        var slice = N / CPUCOUNT;
        wg.Add(CPUCOUNT)
        defer wg.Wait()

        for i := 0; i < CPUCOUNT; i   {
                go func(ii int) {
                        for j := ii * slice; j < ii * slice   slice; j  = 8 {
                                data[j] = twelve(data[j]);
                                data[j 1] = twelve(data[j 1]);
                                data[j 2] = twelve(data[j 2]);
                                data[j 3] = twelve(data[j 3]);
                                data[j 4] = twelve(data[j 4]);
                                data[j 5] = twelve(data[j 5]);
                                data[j 6] = twelve(data[j 6]);
                                data[j 7] = twelve(data[j 7]);
                        }
                        defer wg.Done();
                }(i);
        }
}

func Run(data []float64, N int) {
        populate(data, N);
        start:= time.Now();
        apply(data, N);
        stop:= time.Now();
        elapsed:=stop.Sub(start);
        seconds := float64(elapsed.Milliseconds()) / 1000.0;
        Gflop := float64(N) * 12.0 * 15.0E-9;
        fmt.Printf("%fn", Gflop / seconds);
}

func main() {
        CPUCOUNT := runtime.NumCPU();
        fmt.Printf("num procs : %dn", CPUCOUNT);
        N := 1024*1024*32 * CPUCOUNT;
        data:= make([]float64, N);
        for i := 0; i < 100; i   {
                Run(data, N);
        }
}
 

это попытка перевода из моего бенчмарка c , который дает 80% пиковых флопов.

Версия C выдает 95 GFlop / s, в то время как версия go выдает 6 GFlops / s (счетчик FMA равен 1).

Вот фрагмент сборки go (gccgo -O3 -mfma -mavx2):

 vfmadd132sd     %xmm1, %xmm15, %xmm0
        .loc 1 12 50
        vfmadd132sd     %xmm1, %xmm14, %xmm0
        .loc 1 12 64
        vfmadd132sd     %xmm1, %xmm13, %xmm0
        .loc 1 12 79
        vfmadd132sd     %xmm1, %xmm12, %xmm0
        .loc 1 12 95
        vfmadd132sd     %xmm1, %xmm11, %xmm0
        .loc 1 12 112
        vfmadd132sd     %xmm1, %xmm10, %xmm0
 

И что я получаю из своего кода на c (g -fopenmp -mfma -mavx2 -O3):

 vfmadd213pd     .LC3(%rip), %ymm12, %ymm5
        vfmadd213pd     .LC3(%rip), %ymm11, %ymm4
        vfmadd213pd     .LC3(%rip), %ymm10, %ymm3
        vfmadd213pd     .LC3(%rip), %ymm9, %ymm2
        vfmadd213pd     .LC3(%rip), %ymm8, %ymm1
        vfmadd213pd     .LC3(%rip), %ymm15, %ymm0
        vfmadd213pd     .LC4(%rip), %ymm15, %ymm0
        vfmadd213pd     .LC4(%rip), %ymm14, %ymm7
        vfmadd213pd     .LC4(%rip), %ymm13, %ymm6
        vfmadd213pd     .LC4(%rip), %ymm12, %ymm5
        vfmadd213pd     .LC4(%rip), %ymm11, %ymm4
 

Поэтому у меня есть несколько вопросов, наиболее важным из которых является :

  • Правильно ли я выражаю параллелизм?

а если нет, то как мне это сделать?

Для дополнительного повышения производительности мне нужно знать, что не так со следующими элементами :

  • Почему я вижу только инструкции vfmadd132sd в сборке вместо vfmadd132pd?
  • Как я могу правильно выровнять распределение памяти?
  • Как я могу удалить отладочную информацию из сгенерированного исполняемого файла?
  • Передаю ли я правильные параметры в gccgo?
  • Использую ли я правильный компилятор?

Комментарии:

1. Обратите внимание, что программы обеспечивают параллелизм, а не параллелизм

2. Вы задали слишком много вопросов сразу. Можете ли вы сузить его до одного за раз?

3. @flimzy сузился до первого.

Ответ №1:

Правильно ли я выражаю параллелизм?

Нет. Возможно, вы уничтожаете кэш процессора. (Но это трудно сказать, не зная подробностей о вашей системе. Думаю, это не NUMA?). В любом случае, технически ваш код параллельный, а не параллельный.

Почему я вижу только инструкции vfmadd132sd в сборке вместо vfmadd132pd?

Потому что компилятор поместил его туда. Это вопрос компилятора или вопрос программирования?

Как я могу правильно выровнять распределение памяти?

Это зависит от вашего определения «правильно». Выравнивание полей структуры и среза не контролируется ad hoc, но вы можете изменить порядок полей структуры (которые вы вообще не использовали, поэтому я не знаю, о чем вы здесь спрашиваете).

Как я могу удалить отладочную информацию из сгенерированного исполняемого файла?

Обратитесь к документации gcc.

Передаю ли я правильные параметры в gccgo?

Я не знаю.

Использую ли я правильный компилятор?

Что делает компилятор «правильным»?