Для начала смотрим как обстоят дела у Си.
Пишем такой простой код:
#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>
Комментарии (26)
negasus
12.12.2018 23:34+5Можно, конечно, предсказать комментарий, вида «В три строки делаем HTTP сервер на Go. Си еще есть куда расти»)
Но ведь действительно, разве Go позиционируется, как замена Си, включая производительность?andrei_dm Автор
12.12.2018 23:55-5Ну мне кажется любой язык компилируемый в оп кода процессора можно считать заменой Си
exegete
13.12.2018 00:08+5Уже наличие сборщика мусора в языке автоматически исключает возможность стать заменой языку Си. Также необходим zero runtime, т.е. возможность полного отказа от использования стандартной библиотеки. Иначе просто нереально будет писать ядра операционных систем или прошивки для микроконтроллеров. А это именно те области, где для Си нет альтернатив.
andrei_dm Автор
13.12.2018 00:11Тут я конечно не спорю, но иногда на Си можно написать так, что лучше бы был сборщик мусора
UnknownUser
13.12.2018 12:57При жёстком недостатке памяти очень важно точно контролировать что когда там создаётся и освобождается.
Сборщик мусора при этом будет источником нескончаемой боли разработчика.
А написать так можно на любом языке.
Laney1
13.12.2018 08:58Но ведь действительно, разве Go позиционируется, как замена Си, включая производительность?
Go позиционируется как замена Си в программах, где бутылочным горлышком оказывается скорость не собственно кода, а системных вызовов, ввода-вывода, и т.п. Например, в таких как docker.
В софте, где идет борьба за каждую ассемблерную инструкцию, разумеется никакого Go быть не может. Большой привет автору статьи. Там даже на C++ часто ругаются.
FenixFly
12.12.2018 23:43+4Некорректно приводить сравнения, в которых действия выполняются меньше секунды. А вдруг это просто система лагнула?
andrei_dm Автор
12.12.2018 23:54-1результат стабильный
tangro
13.12.2018 11:49О стабильности результата можно было бы говорить, если бы Вы привели ну там 1000 экспериментов, показали матожидание, дисперсию, расказали о своём подходе к минимизации посторонних факторов и т.д. А так тест выглядит почти как случайные данные.
zuko3d
13.12.2018 00:33Предлагаю скомпилировать сишный код с "-Ofast -march=native", ассемблерный код получится намного интереснее. Помимо этого, если у вас проц от Intel, имеет смысл скомпилить через icc.
andrei_dm Автор
13.12.2018 00:35тот же результат
0x0000555555554590 <+32>: add $0x1,%eax
0x0000555555554593 <+35>: pxor %xmm0,%xmm1
0x0000555555554597 <+39>: paddq %xmm2,%xmm0
0x000055555555459b <+43>: cmp $0x4c4b40,%eaxapro
13.12.2018 04:03А у меня результат (gcc 8.2.1) выглядит так:
(gdb) disassemble main Dump of assembler code for function main: 0x0000000000001040 <+0>: push %rbp 0x0000000000001041 <+1>: vmovdqa 0xfd7(%rip),%ymm0 # 0x2020 0x0000000000001049 <+9>: xor %eax,%eax 0x000000000000104b <+11>: vpxor %xmm3,%xmm3,%xmm3 0x000000000000104f <+15>: vmovdqa 0xfe9(%rip),%xmm4 # 0x2040 0x0000000000001057 <+23>: mov %rsp,%rbp 0x000000000000105a <+26>: and $0xffffffffffffffe0,%rsp 0x000000000000105e <+30>: xchg %ax,%ax 0x0000000000001060 <+32>: vextractf128 $0x1,%ymm0,%xmm1 0x0000000000001066 <+38>: vpaddq %xmm0,%xmm4,%xmm2 0x000000000000106a <+42>: vpaddq %xmm1,%xmm4,%xmm1 0x000000000000106e <+46>: add $0x1,%eax 0x0000000000001071 <+49>: vxorps %ymm0,%ymm3,%ymm3 0x0000000000001075 <+53>: vinsertf128 $0x1,%xmm1,%ymm2,%ymm0 0x000000000000107b <+59>: cmp $0x2625a0,%eax 0x0000000000001080 <+64>: jne 0x1060 <main+32> 0x0000000000001082 <+66>: lea 0xf7b(%rip),%rdi # 0x2004 0x0000000000001089 <+73>: vmovdqa %xmm3,%xmm0 0x000000000000108d <+77>: xor %eax,%eax 0x000000000000108f <+79>: vextractf128 $0x1,%ymm3,%xmm3 0x0000000000001095 <+85>: vpxor %xmm3,%xmm0,%xmm3 0x0000000000001099 <+89>: vpsrldq $0x8,%xmm3,%xmm0 0x000000000000109e <+94>: vpxor %xmm0,%xmm3,%xmm3 0x00000000000010a2 <+98>: vmovq %xmm3,%rsi 0x00000000000010a7 <+103>: vzeroupper 0x00000000000010aa <+106>: callq 0x1030 <printf@plt> 0x00000000000010af <+111>: xor %eax,%eax 0x00000000000010b1 <+113>: leaveq 0x00000000000010b2 <+114>: retq End of assembler dump.
quasilyte
13.12.2018 00:37В компиляторе gc (который знаком большинству людей) нет векторизации циклов. Вообще. By design. Никто пока не предложил как её внедрить, чтобы не нарушить одно из:
1. Не замедляет время компиляции.
2. Реализация не слишком сложная (maintainability).
3. Имеет примеры важного кода, который будет сильно ускоряться, кроме синтетики.
Я не утверждаю, что это бесполезные вещи, просто напоминаю, что приоритет у этого всего довольно низкий, а порог для включения этих оптимизаций в ядро Go довольно высокий. Как-то так.
Математический код и HPC может и получит буст от векторизации, но для gc по-моему это не самые частые пользователи. Возможно в будущем что-то изменится, но пока ситуация такая. Где-то ещё были разные связанные с этим proposal'ы, в том числе о введении примитивов для использования FMA инструкций, но под рукой списка нет, можете поискать на github трекере, если интересно.
Возможно LLVM-based компилятор будет лучше, но по-моему там пока ещё не достаточно всё зрелое.andrei_dm Автор
13.12.2018 00:49Завтра попробую intel компилятор, но по факту уже чистые sse2 инструкции дают хороший результат, хотя в большинстве прикладных задач, конечно этого не требуется
Nagg
13.12.2018 03:53LLVM успешно развернет и завекторизует.
Т.е. даже C# с бэкендом LLVM через mono-llvm или Unity Burst будет быстрее
PS: вообще я удивлен почему компиляторы не посчитали цикл в компайл тайме и не заменили константой — возможно через какой-нибудь llvm-souper/polly можно оптимизнутьRPG18
13.12.2018 11:49За все надо платить. Стековая природа стандартного компилятора позволяет быстро переключать горутины. Как только мы хотим использовать больше инструкций процессора, то увеличивается контекст(количество регистров которые нужно сохранить).
Nagg
13.12.2018 03:49Скомпилировал ваш Си код clang с -O2 -march=native (или -mcpu=haswell) — он развернул ваш цикл, распихал все инты по всем доступным AVX регистрам.
https://godbolt.org/z/iLkRS5
ЗЫ: даже без avx все равно будет быстрее — будет тот же анроллинг и все SSE регистры
real 0m0.003s
user 0m0.001s
sys 0m0.001s
MainNika
13.12.2018 10:49+1Код который отвечает за цикл при компиляциях gccgo vs gcc одинаков абсолютно. gccgo с -O3 тоже вкорячивает те же avx инструкции. Посмотрите godbolt.org/z/CvFV-X
Вы в итоге хотите сравнить циклы C vs Go а на деле сравниваете накладные расходы на запуск приложения, на гошный рантайм, на специфику работы printf.
Да и на самом деле смысл Go это больше про скорость разработки а не про тесты производительности с C.andrei_dm Автор
13.12.2018 12:51Да я согласен, время тут сравнивать совсем не корректно, go запускает рабочие треды, шедулер, сборщик мусора, причем родной компилятор Go делает запуск быстрее. И скорее всего исходя из опыта использования google tcmalloc vs стандартный malloc, в много-тредовых сервисах go может показать сравнимые, а может и лучшие результаты относительно сишных сервисов основанных на обычном маллоке и posix тредах.
andrei_dm Автор
13.12.2018 13:51Все же удалось использовать SSE с float типом, видимо с int компилятор считает это преждевременной оптимизацией
a-tk
13.12.2018 21:12Видимо в связи с тем, что штатный компилятор отлично оптимизирует запуск тредов (в моем случае на 4 ядра было создано 5 дополнительных тредов), приложение отрабатывает быстрее.
У меня одного эта фраза вызвала лёгкое недоумение?
youROCK
Ну Go всё-таки не предназначен для расчетных задач и это не приоритет у разработчиков. Если вам очень хочется оптимизировать конкретный кусок, то лучше использовать встроенный ассемблер (но это непортируемое решение, конечно же)
selenite
> непортируемое
можно выехать за счет build tags