В benchmark Go нагрузка распределяется через RunParallel. Как интерпретировать SetParallelism и почему его значение нельзя считать числом одновременно выполняющихся горутин?
SetParallelism задаёт множитель для числа рабочих горутин, которые использует RunParallel: целевое число обычно рассчитывается как значение параметра, умноженное на текущий GOMAXPROCS. Это не число одновременно исполняющихся горутин: GOMAXPROCS ограничивает число горутин, которые могут одновременно выполнять Go-код на процессорах, а остальные могут ожидать планирования или блокировки.
Обычный benchmark выполняет операции последовательно, поэтому плохо показывает поведение кода при конкуренции за общий ресурс: mutex, пул соединений, кэш или атомарную переменную. RunParallel появился как стандартный способ распределить итерации benchmark между рабочими горутинами и измерить пропускную способность конкурентной операции.
Разделение числа рабочих горутин и числа одновременно исполняющихся потоков необходимо, потому что эти параметры описывают разные свойства нагрузки. В реальном сервисе клиентов может быть значительно больше, чем доступных процессоров.
Если принять значение SetParallelism за количество одновременно работающих горутин, можно неверно настроить эксперимент. Например, при GOMAXPROCS, равном 8, значение параллелизма 2 ориентирует RunParallel примерно на 16 рабочих горутин, но одновременно исполнять Go-код обычно смогут не более 8 из них.
Слишком маленькая нагрузка может скрыть конкуренцию и блокировки. Слишком большая — создать искусственное перепланирование, очереди и задержки, которых нет в моделируемом сценарии. Поэтому результат benchmark имеет смысл только вместе с указанными GOMAXPROCS, уровнем параллелизма и характеристиками тестовой машины.
При запуске RunParallel тестовый фреймворк создаёт рабочие горутины. Каждая из них многократно вызывает тело benchmark, пока общий объём работы не достигнет рассчитанного значения b.N; отдельная итерация не обязана выполняться в отдельной горутине.
SetParallelism(p) задаёт множитель для числа рабочих горутин относительно текущего GOMAXPROCS. Значение по умолчанию — 1, поэтому типичный запуск ориентирован примерно на одну рабочую горутину на доступную Go-программе единицу процессорного параллелизма.
В этом примере значение 2 не означает «две одновременно исполняющиеся горутины». При GOMAXPROCS, равном 4, benchmark ориентируется примерно на 8 рабочих горутин. Однако одновременно выполнять Go-код сможет не более числа, разрешённого GOMAXPROCS; часть горутин может блокироваться или ждать планировщика.
SetParallelism не изменяет GOMAXPROCS и не задаёт число операционных потоков. Он также влияет только на рабочие горутины RunParallel, а не на все горутины процесса и не на число повторных запусков benchmark при разных значениях флага -cpu.
Для сравнения реализаций нужно сохранять одинаковую модель нагрузки. Если у одного benchmark параллелизм равен 1, а у другого — 8, их результаты описывают разные режимы работы и не дают честного сравнения без дополнительного объяснения.
Команда измеряла конкурентный кэш. Последовательный benchmark был простым и стабильным, но не показывал деградацию при конкуренции за блокировку. Вариант с обычным циклом оказался непригоден для этой цели, потому что измерял только последовательный путь; вариант с RunParallel по умолчанию лучше отражал нагрузку на машине разработчика, но не моделировал большое число клиентов.
Команда выбрала несколько отдельных benchmark-сценариев с явно зафиксированными уровнями параллелизма. Это позволило увидеть поведение кэша при нагрузке, близкой к числу процессоров, и при избытке конкурентных клиентов. Такой подход сложнее и требует больше времени на интерпретацию, зато не смешивает пропускную способность при разных режимах нагрузки в один показатель.
Вопрос: Гарантирует ли RunParallel, что каждая итерация benchmark выполняется в новой горутине?
Ответ: Нет. Рабочая горутина получает объект PB и вызывает Next() в цикле. Один worker выполняет множество итераций, поэтому создание горутины не является частью каждой измеряемой операции. Общее число итераций распределяется между workers, а не преобразуется в такое же количество горутин.
Вопрос: Изменит ли SetParallelism значение GOMAXPROCS?
Ответ: Нет. SetParallelism изменяет количество рабочих горутин, используемых RunParallel, но не число процессоров, на которых планировщик может одновременно исполнять Go-код. Если нужно исследовать влияние числа процессоров, это делают отдельными запусками с разными настройками GOMAXPROCS, явно фиксируя их в результатах.
Вопрос: Можно ли напрямую сравнить результаты двух RunParallel-benchmark с разными значениями SetParallelism?
Ответ: Обычно нет. Разные значения задают разные уровни конкуренции, поэтому меняются очереди, содержание блокировок, промахи кэша и доля перепланирования. Такое сравнение допустимо как исследование зависимости производительности от нагрузки, но не как утверждение, что одна реализация быстрее другой в одинаковых условиях.