Pull to refresh

Cи vs Go циклы и простая математика

Reading time2 min
Views10K
Когда я устал от программирования на Си, как и многих, меня заинтересовал язык Go. Он строго типизирован, компилируемый, следовательно достаточно производителен. И тут мне захотелось узнать, насколько заморочились создатели Go над оптимизацией работы с циклами и числами.

Для начала смотрим как обстоят дела у Си.

Пишем такой простой код:

#include <stdint.h>
#include <stdio.h>

int main()
{
        uint64_t i;
        uint64_t j = 0;
        for ( i = 10000000; i>0; i--)
        {
                j ^= i;
        }
        printf("%lu\n", j);
        return 0;
}

Компилируем с O2, дизассемблируем:

564:   31 d2                   xor    %edx,%edx
566:   b8 80 96 98 00          mov    $0x989680,%eax
56b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
570:   48 31 c2                xor    %rax,%rdx
573:   48 83 e8 01             sub    $0x1,%rax
577:   75 f7                   jne    570 <main+0x10>

Получаем время исполнения:

real 0m0,023s
user 0m0,019s
sys 0m0,004s

Казалось бы уже не куда ускорятся, но у нас же современный процессор, для таких операций у нас есть быстрые sse регистры. Пробуем опции gcc -mfpmath=sse -msse4.2 результат тот же.
Добавляем -O3 и ура:

 57a:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
 580:   83 c0 01                add    $0x1,%eax
 583:   66 0f ef c8             pxor   %xmm0,%xmm1
 587:   66 0f d4 c2             paddq  %xmm2,%xmm0
 58b:   3d 40 4b 4c 00          cmp    $0x4c4b40,%eax
 590:   75 ee                   jne    580 <main+0x20>

Видно, что используется SSE2 команды и SSE регистры, и получаем тройной прирост производительности:

real 0m0,006s
user 0m0,006s
sys 0m0,000s

Тоже на Go:

package main
  
import "fmt"

func main() {
        i := 0
        j := 0
        for i = 10000000; i>0; i-- {
                j ^= i
        }
        fmt.Println(j)
}

0x000000000048211a <+42>:    lea    -0x1(%rax),%rdx
0x000000000048211e <+46>:    xor    %rax,%rcx
0x0000000000482121 <+49>:    mov    %rdx,%rax
0x0000000000482124 <+52>:    test   %rax,%rax
0x0000000000482127 <+55>:    ja     0x48211a <main.main+42>


Тайминги Go:
штатный go:
real 0m0,021s
user 0m0,018s
sys 0m0,004s

gccgo:
real 0m0,058s
user 0m0,036s
sys 0m0,014s

Производительность как в случае Си и O2, также ставил gccgo результат такой же, но работает дольше штатного Go (1.10.4) компилятора. Видимо в связи с тем, что штатный компилятор отлично оптимизирует запуск тредов (в моем случае на 4 ядра было создано 5 дополнительных тредов), приложение отрабатывает быстрее.

Заключение



Мне все же удалось заставить стандартный компилятор Go работать c sse инструкциями для цикла, подсунув ему родной для sse float.

package main
  
// +build amd64

import "fmt"

func main() {
        var i float64 = 0
        var j float64 = 0
        for i = 10000000; i>0; i-- {
                j += i
        }
        fmt.Println(j)
}


0x0000000000484bbe <+46>: movsd 0x4252a(%rip),%xmm3 # 0x4c70f0 <$f64.3ff0000000000000>
0x0000000000484bc6 <+54>: movups %xmm0,%xmm4
0x0000000000484bc9 <+57>: subsd %xmm3,%xmm0
0x0000000000484bcd <+61>: addsd %xmm4,%xmm1
0x0000000000484bd1 <+65>: xorps %xmm2,%xmm2
0x0000000000484bd4 <+68>: ucomisd %xmm2,%xmm0
0x0000000000484bd8 <+72>: ja 0x484bbe <main.main+46>
Tags:
Hubs:
Total votes 38: ↑9 and ↓29-20
Comments26

Articles