Go: производительность горутин

http://en.munknex.net/2011/12/golang-goroutines-performance.html
  • Перевод

Введение


В этом посте мы рассмотрим производительность горутин (goroutine). Горутины — это нечто в роде очень дешевых и легковесных потоков. Больше всего, наверное, они похожи на процессы в Erlang.

Согласно документации мы можем использовать сотни тысяч горутин в наших программах. И цель статьи — проверить и конкретизировать это.

Память


Размер памяти, выделяемой для горутины, не документирован (говорится только, что это несколько килобайт), но тесты на разных машинах и множество подтверждений в интернете позволяет уточнить это число до 4 — 4,5 килобайт. То есть 5 Гб вам с запасом хватит на 1 миллион горутин.

Производительность


Остается определиться с тем, сколько процессорного времени мы теряем, когда выделяем код в горутину. Напомню, что для это только нужно поставить ключевое слово go перед вызовом функции.

go testFunc()

Горутины — это в первую очередь средства достижения многозадачности. По умолчанию, если в системе не установлена переменная GOMAXPROCS, программа использует только один поток. Чтобы задействовать все ядра процессора, нужно записать в нее их количество: export GOMAXPROCS=2. Переменная считывается во время исполнения, так что перекомпилировывать программу после каждого её изменения не придётся.

Получается, время затрачивается на создание горутин, переключение между ними, а еще иногда на перемещение в другой поток и передачу сообщений между горутинами в разных потоках. Чтобы избежать последнего, начнём тестирование только с одного потока.

Все действия производятся на неттопе с:
  • Atom D525 Dual Core 1.8 GHz
  • 4Gb DDR3
  • Go r60.3
  • Arch Linux x86_64

Методика


Вот генератор исследуемых функций:

func genTest (n int) func (res chan <- interface {}) {
        return func(res chan <- interface {}) {
                for i := 0; i < n; i++ {
                        math.Sqrt(13)
                }
                res <- true
        }
}


А вот набор полученный функций, вычилсяющих корень из 13 по 1, 10, 100, 1000 и 5000 раз соответственно:

testFuncs := [] func (chan <- interface {}) { genTest(1), genTest(10), genTest(100), genTest(1000), genTest(5000) }

Теперь, каждую функцию я запускаю X раз в цикле, а потом в X горутинах. А затем сравниваю затраченное время. Кроме того не стоит забывать про сборку мусора. Чтобы минимизировать влияние на результаты, я явно вызываю её после того, как отработают все горутины и только потом отмечаю конец операции.

Ну и, разумеется, для точности каждый тест проводится много раз. Общее время выполнения программы заняло около 16 часов.

Один поток


export GOMAXPROCS=1
gorounes performance 1_1

Из графика видно, что функция, время выполнения которой примерно равно вычислению корня, при выделении в горутину потратит примерно в 4 раза больше времени.

Рассмотрим 4 оставшиеся функции по-подробнее:
gorounes performance 1_2

Видно, что даже при 700 тысячах одновременно работающих горутин производительность не падает больше чем на 80%. Самое классное, что уже при времени работы функции примерно равном вычислению sqrt(13) 1000 раз, оверхед составляет всего лишь ~2%. А при 5000 раз — всего 1%! И эти значения, похоже, практически не зависят от количества работающих горутин! То есть единственное ограничение — память.

Вывод:

Если независимый участок кода будет выполняться (включая время ожидания) больше чем вычисление 10 корней, и вы хотите выполнить его параллельно, то смело выделяйте его в горутину. Хотя если безболезненно удастся собрать вместе 10 или даже 100 таких участков, то потери производительности составят всего 20% или 2% соответственно.

Несколько потоков


Теперь рассмотрим ситуацию, когда мы хотим использовать сразу несколько ядер процессора. В моём случае их всего 2:

export GOMAXPROCS=2

Теперь выполним тестирующую программу снова:
gorounes performance 2_1

Тут хорошо видно, что несмотря на то, что количество ядер удвоилось, время работы первых двух функций — наоборот ухудшилось! Пускай и незначительно. Это объясняется тем, что затраты на перенос их в другой поток больше, чем на выполнение :)

Пока планировщик не может разруливать подобные ситуации, но авторы Go обещают в будущем исправить такую недоработку.
gorounes performance 2_1

А вот тут можно рассмотреть, что последние две функции используют оба ядра почти на полную катушку. На моём неттопе, каждая отдельная функция выполняется за ~45мкс и ~230мкс соответственно.

Заключение


Даже несмотря на молодость языка и временную недостаточно хорошую реализацию планировщика, производительность очень радует. Особенно в сочетании с простотой использования.

В качестве совета могу предложить стараться не использовать в качестве горутин функции работающие меньше 1 микросекунды. И смело использовать работающие больше 1 миллисекунды :)

P.S. Было бы хорошо увидеть аналогичные тесты на других языках, например на эрланге. Википедия сообщает об успешных попытках запускать на нём до 20 миллионов процессов!
  • +50
  • 9,5k
  • 6
Поделиться публикацией
Комментарии 6
    +4
    Спасибо! Побольше бы статей про Go. С новым годом =)!
      +6
      Присоединяюсь! Пора создавать хабрасообщество любителей Go.

      А вообще круто что даже 31го декабря глубоко под вечер появляются такие статьи, спасибо автор! С новым годом!
        +4
        Горутины — это нечто в роде очень дешевых и легковесных потоков. Больше всего, наверное, они похожи на процессы в Erlang


        Уж очень поверхностное объяснение для программистов. И когда говорят про легковесные потоки в Erlang складывается ощущение какой-то магии. Но недавно столкнулся с темой легковесных потоков и на самом деле все проще некуда.

        Легковесные потоки это ни какая ни магия, а простая эмуляция потоков в одном или нескольких нативных потоках. Еще известная как green threads. Сделаны они для того, чтобы устранить проблему переключения огромного количества нативных поток в ОС. На практике может произойти ситуация, что время на переключение между множеством нативных (тяжелых) поток может уходить больше, чем на саму полезную работу. Поэтому и стали использовать green threads, когда нужно много потоков, а ядер очень мало.

        И кстати, в Java, например, раньше были именно легковесные потоки, но затем их убрали и сделали нативные. А в Solaris можно и сегодня выбрать как будет работать Java, через green theads или нативно.
          +1
          И всё-таки разница есть. У вас ведь не получится запустить 700k одновременно работающих ( с потерей производительности до 2% ) зелёных потоков в Java на домашнем компьютере ;)

          Насколько я помню, они потому и были убраны, что работали медленнее системных потоков.

          А горутины — это скорее вариация сопрограмм ( coroutine ), общающихся между собой сообщениями:

          Don't communicate by sharing memory; share memory by communicating.

          Но авторы всё-таки отступились от начальной концепции, поэтому подобрали новое название.

            0
            Насчет производительности не в курсе. Несомненно, авторы Go хорошо продумали свои горутины и хорошо их оптимизировали и оптимизируют в будущем. Как-никак горутины это одна из важных фишек Go.

            Но и в Scala (и Java) есть нечто похожее — Akka
              0
              Думаю да. Хотя, честно говоря, я не знаток Scala. Но из того, что я читал, у меня сложилось впечатление, что Акторы — это несколько более высокоуровневая штука.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое