Оптимизирован для типа int64 в Golang.abs()
функция, оригинал:Optimized abs() for int64 in Go
предисловие
В языке Go нет встроенногоabs()
Стандартные функции для вычисления абсолютного значения целого числа, где абсолютное значение относится к неотрицательному представлению отрицательных и положительных чисел.
я недавно решилAdvent of Code 2017вершинаDay 20проблема, понял одинabs()
функция. Если вы хотите узнать что-то новое или попробовать свои силы в этом, проверьте это.
Go на самом деле ужеmath
реализовано в пакетеabs()
: math.Abs, но это не относится к моей проблеме, потому что его типы входных и выходных значений обаfloat64
, что мне нужно этоint64
. Можно использовать преобразование параметров, но изменитьfloat64
Преобразовать вint64
Будут некоторые накладные расходы, и числа с большими значениями конверсии будут усечены, и то, и другое будет разъяснено в статье.
ПочтаPure Go math.Abs outperforms assembly versionОбсуждается, как оптимизировать числа с плавающей запятой.math.Abs
, но эти методы оптимизации нельзя напрямую применить к целым числам из-за различных базовых кодировок.
Исходный код и тестовые случаи в статье находятся вcavaliercoder/go-abs
Преобразование типов VS метод управления ветвями
Для меня простейшая реализация функции для получения абсолютного значения: если входной параметр n больше или равен 0, он возвращает n напрямую, а если он меньше нуля, он возвращает -n (отрицательное число обратно положительное ) Эта функция получения абсолютного значения зависит от структуры управления ветвями для расчета. Абсолютное значение называется:abs.WithBranch
package abs
func WithBranch(n int64) int64 {
if n < 0 {
return -n
}
return n
}
успешно возвращает абсолютное значение n, т.е.Go v1.9.x math.Abs
Реализация, которая принимает абсолютное значение float64. Однако, когда выполняется преобразование типа (int64 в float64) и берется абсолютное значение, есть ли улучшения в версии 1.9.x? Мы можем это проверить:
package abs
func WithStdLib(n int64) int64 {
return int64(math.Abs(float64(n)))
}
В приведенном выше коде измените n сint64
Превратиться вfloat64
,пройти черезmath.Abs
Возьмите абсолютное значение, а затем вернитесьint64
, многочисленные преобразования, очевидно, влекут за собой снижение производительности. Вы можете написать тест, чтобы проверить это:
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/cavaliercoder/abs
BenchmarkWithBranch-8 2000000000 0.30 ns/op
BenchmarkWithStdLib-8 2000000000 0.79 ns/op
PASS
ok github.com/cavaliercoder/abs 2.320s
Результат теста: 0,3 нс/оп,WithBranch
Он более чем в два раза быстрее и имеет то преимущество, что при преобразовании больших чисел из int64 в float64 в стандарте IEEE-754 не происходит усечения (потери неточных значений).
Например:abs.WithBranch(-9223372036854775807)
правильно вернет 9223372036854775807. ноWithStdLib(-9223372036854775807)
Затем происходит переполнение в диапазоне преобразования типов и возвращается -9223372036854775808. Когда вводится большое положительное число,WithStdLib(9223372036854775807)
Также возвращает неверные отрицательные результаты.
Метод, который не использует управление ветвлениями для получения абсолютного значения, очевидно, быстрее и точнее для целых чисел со знаком, но есть ли лучший способ?
Все мы знаем, что код, который не опирается на методы управления ветвлениями, искажает порядок выполнения программы, т.е.pipelining processorsСледующее действие программы невозможно предсказать.
Сценарии, отличающиеся от подходов, не основанных на управлении ветвями
Восторг хакераВ главе 2 представлен метод безответственного управления с помощьюДополнение до двухВычисляет абсолютное значение целого числа со знаком.
Чтобы вычислить абсолютное значение x:
-
Рассчитать сначала
x >> 63
, то есть x сдвигается вправо на 63 бита (чтобы получить старший бит знака).Если вы знакомы с целыми числами без знака, вы должны знать, что если x отрицательное, то y равно 1, иначе y равно 0 -
пересчитывать
(x ⨁ y) - y
: XOR x и y минус y является абсолютным значением x.Его можно реализовать напрямую с помощью эффективного ассемблера, код выглядит следующим образом:
func WithASM(n int64) int64
// abs_amd64.s
TEXT ·WithASM(SB),$0
MOVQ n+0(FP), AX // copy input to AX
MOVQ AX, CX // y ← x
SARQ $63, CX // y ← y >> 63
XORQ CX, AX // x ← x ⨁ y
SUBQ CX, AX // x ← x - y
MOVQ AX, ret+8(FP) // copy result to return value
RET
Сначала назовем эту функцию какWithASM
, отдельное наименование и реализация, использование тела функцииСборка в GoРеализация, приведенный выше код применим только к системе с архитектурой AMD64, я предлагаю вам добавить имя файла_amd64.s
суффикс.
WithASM
Результаты тестов для:
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/cavaliercoder/abs
BenchmarkWithBranch-8 2000000000 0.29 ns/op
BenchmarkWithStdLib-8 2000000000 0.78 ns/op
BenchmarkWithASM-8 2000000000 1.78 ns/op
PASS
ok github.com/cavaliercoder/abs 6.059s
Это смущает, этот простой бенчмарк показывает, что код с очень компактной структурой управления без ветвлений работает очень медленно: 1,78 нс/оп, как такое может быть?
параметры компиляции
Нам нужно знать, как компилятор Go оптимизирует выполнениеWithASM
функция, компилятор принимает-m
параметр для печати оптимизированного содержимого, вgo build
илиgo test
плюс-gcflags=-m
использовать:
текущий результат:
$ go tool compile -m abs.go
# github.com/cavaliercoder/abs
./abs.go:11:6: can inline WithBranch
./abs.go:21:6: can inline WithStdLib
./abs.go:22:23: inlining call to math.Abs
Для нашей простой функции компилятор Go поддерживаетfunction inlining, встраивание функции означает использование тела этой функции напрямую вместо вызова нашей функции. Например:
package main
import (
"fmt"
"github.com/cavaliercoder/abs"
)
func main() {
n := abs.WithBranch(-1)
fmt.Println(n)
}
будет фактически скомпилирован в:
package main
import "fmt"
func main() {
n := -1
if n < 0 {
n = -n
}
fmt.Println(n)
}
По выводу компилятора видно, чтоWithBranch
иWithStdLib
встраивается во время компиляции, ноWithASM
нет. заWithStdLib
, даже если базовый вызовmath.Abs
Но он все еще встроен во время компиляции.
так какWithASM
Функции не могут быть встроены, и каждая вызывающая их функция влечет за собой дополнительные накладные расходы на вызов:WithASM
Перераспределить память стека, скопировать параметры и указатели и т. д.
Что, если мы не будем использовать встраивание в других функциях? Можно написать простую примерную программу:
package abs
//go:noinline
func WithBranch(n int64) int64 {
if n < 0 {
return -n
}
return n
}
Перекомпилируйте, и мы увидим, что компилятор меньше оптимизирует:
$ go tool compile -m abs.go
abs.go:22:23: inlining call to math.Abs
Сравнительные результаты:
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/cavaliercoder/abs
BenchmarkWithBranch-8 1000000000 1.87 ns/op
BenchmarkWithStdLib-8 1000000000 1.94 ns/op
BenchmarkWithASM-8 2000000000 1.84 ns/op
PASS
ok github.com/cavaliercoder/abs 8.122s
Видно, что среднее время выполнения трех функций теперь составляет около 1,9 нс/операцию.
Вы можете подумать, что накладные расходы на вызов каждой функции составляют около 1,5 нс, появление этих накладных расходов нас сводит на нет.WithBranch
Преимущество скорости в функциях.
То, что я узнал из выше, это то,WithASM
Производительность лучше, чем производительность, обеспечиваемая компилятором реализацией безопасности типов, сборкой мусора и встраиванием функций, хотя в большинстве случаев этот вывод может быть неверным. Конечно, есть исключения из этого, такие какSIMDпроизводительность шифрования, кодирование потокового мультимедиа и т. д.
Используйте только одну встроенную функцию
Компилятор Go не может встраивать функции, реализованные на ассемблере, но нашу обычную функцию легко встроить после перезаписи:
package abs
func WithTwosComplement(n int64) int64 {
y := n >> 63 // y ← x >> 63
return (n ^ y) - y // (x ⨁ y) - y
}
Результат компиляции показывает, что наш метод встроенный:
$ go tool compile -m abs.go
...
abs.go:26:6: can inline WithTwosComplement
Но как насчет производительности? Результаты показывают, что когда мы разрешаем встраивание функций, производительность такая же, как и уWithBranch
Очень близко:
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/cavaliercoder/abs
BenchmarkWithBranch-8 2000000000 0.29 ns/op
BenchmarkWithStdLib-8 2000000000 0.79 ns/op
BenchmarkWithTwosComplement-8 2000000000 0.29 ns/op
BenchmarkWithASM-8 2000000000 1.83 ns/op
PASS
ok github.com/cavaliercoder/abs 6.777s
Теперь накладные расходы на вызов функции исчезли,WithTwosComplement
реализация, чемWithASM
реализация намного лучше. Посмотрим, что компилирует компилятор.WithASM
что ты сделал?
использовать-S
Аргумент говорит компилятору распечатать процесс сборки:
$ go tool compile -S abs.go
...
"".WithTwosComplement STEXT nosplit size=24 args=0x10 locals=0x0
0x0000 00000 (abs.go:26) TEXT "".WithTwosComplement(SB), NOSPLIT, $0-16
0x0000 00000 (abs.go:26) FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
0x0000 00000 (abs.go:26) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (abs.go:26) MOVQ "".n+8(SP), AX
0x0005 00005 (abs.go:26) MOVQ AX, CX
0x0008 00008 (abs.go:27) SARQ $63, AX
0x000c 00012 (abs.go:28) XORQ AX, CX
0x000f 00015 (abs.go:28) SUBQ AX, CX
0x0012 00018 (abs.go:28) MOVQ CX, "".~r1+16(SP)
0x0017 00023 (abs.go:28) RET
...
компилятор компилируетWithASM
иWithTwosComplement
В это время то, что сделано, слишком похоже, и компилятор имеет преимущество правильной настройки и кроссплатформенности в это время.Можно добавитьGOARCH=386
возможность повторной компиляции для создания программ, совместимых с 32-битными системами.
Наконец, что касается распределения памяти, реализация всех вышеперечисленных функций — идеальная ситуация.go test -bench=. -benchme
, и наблюдение за выводом каждой функции показывает, что выделения памяти не произошло.
Суммировать
WithTwosComplement
Реализация обеспечивает лучшую переносимость в Go, обеспечивая встраивание функций, код без ветвлений, нулевое выделение памяти и избегая усечения значений, вызванного преобразованием типов. Тесты не показывают преимущества управления без ветвления по сравнению с управлением с ветвлениями, но теоретически код без управления ветвлениями во многих случаях будет работать лучше.
Наконец, моя реализация abs для int64 выглядит следующим образом:
func abs(n int64) int64 {
y := n >> 63
return (n ^ y) - y
}
Наконец
Это первая статья, в переводе которой я участвовал после присоединения к GCTT, спасибо автору оригинала.Ryan Armstrong. Я научился писать тест-кейсы, правильно использовать бенчмарк-тесты и т. д. Постараюсь в будущем каждую неделю качественно переводить блог :-D
с помощью:Optimized abs() for int64 in Go
автор:Ryan Armstrong
Переводчик:wuYinBest
Вычитка:rxcai