Руководство
|
Страница в процессе перевода. |
Пакет BenchmarkTools был создан для упрощения следующих задач:
-
объединение наборов тестов производительности в управляемые пакеты;
-
настройка, сохранение и повторная загрузка параметров тестов производительности для повышения удобства, точности и согласованности;
-
выполнение тестов производительности с получением обоснованных и согласованных прогнозов;
-
анализ и сравнение результатов с целью определения того, привело ли изменение кода к ухудшению или улучшению ситуации.
Прежде чем двигаться дальше, давайте дадим определения некоторым терминам, используемым в этом документе.
-
«Вычисление»: однократное выполнение выражения теста производительности.
-
«Проба»: однократное измерение времени или памяти, полученное в результате нескольких вычислений.
-
«Испытание»: эксперимент, в рамках которого собирается несколько проб (или результат такого эксперимента).
-
«Параметры теста производительности»: параметры конфигурации, определяющие способ проведения испытания производительности.
Обоснование нашего определения термина «проба» может быть неочевидным для некоторых читателей. Если время выполнения теста производительности меньше разрешения используемого метода измерения времени, то однократное вычисление теста, как правило, не даст достоверной пробы. В этом случае необходимо приблизительно определить достоверную пробу, зафиксировав общее время t, необходимое для регистрации n вычислений, и оценив время, затрачиваемое на одно вычисление пробы, как t/n. Например, если при определении пробы затрачивается 1 секунда на 1 миллион вычислений, то приблизительное время выполнения одного вычисления для пробы составит 1 микросекунду. Не всегда очевидно, какое количество вычислений на пробу следует использовать для того или иного теста производительности, поэтому BenchmarkTools предоставляет механизм (метод tune!), который автоматически выводит это значение.
Основные сведения о тестах производительности
Определение и выполнение тестов производительности
Чтобы быстро протестировать выражение Julia, используйте макрос @benchmark:
julia> @benchmark sin(1)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
Range (min … max): 1.442 ns … 53.028 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 1.453 ns ┊ GC (median): 0.00%
Time (mean ± σ): 1.462 ns ± 0.566 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
█
▂▁▁▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█▁▁█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▃▁▁▃
1.44 ns Histogram: frequency by time 1.46 ns (top 1%)
Memory estimate: 0 bytes, allocs estimate: 0.
Макрос @benchmark позволяет выполнить сразу несколько задач: определить тест производительности, автоматически настроить его параметры конфигурации и запустить тест. Эти три этапа можно также выполнять по отдельности явным образом с помощью @benchmarkable, tune! и run:
julia> b = @benchmarkable sin(1); # определяем тест производительности с параметрами по умолчанию
# определяем подходящее количество вычислений на пробу и число проб для этого теста производительности
julia> tune!(b);
julia> run(b)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
Range (min … max): 1.442 ns … 4.308 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 1.453 ns ┊ GC (median): 0.00%
Time (mean ± σ): 1.456 ns ± 0.056 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
█
▂▁▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█▁▁█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂▁▁▃
1.44 ns Histogram: frequency by time 1.46 ns (top 1%)
Memory estimate: 0 bytes, allocs estimate: 0.
В качестве альтернативы можно использовать макросы @btime, @btimed, @belapsed, @ballocated или @ballocations. Они принимают точно такие же аргументы, как и @benchmark, но работают как макросы @time, @timed, @elapsed, @allocated и @allocations, входящие в состав Julia.
julia> @btime sin(1)
13.612 ns (0 allocations: 0 bytes)
0.8414709848078965
julia> @belapsed sin(1)
1.3614228456913828e-8
julia> @btimed sin(1)
(value = 0.8414709848078965, time = 9.16e-10, bytes = 0, alloc = 0, gctime = 0.0)
julia> @ballocated rand(4, 4)
208
julia> @ballocations rand(4, 4)
2
Параметры (Parameters) теста производительности
Для настройки процесса выполнения в @benchmark, @benchmarkable и run можно передать следующие именованные аргументы:
-
samples: количество проб. После сбора данного количества проб выполнение завершается. По умолчаниюBenchmarkTools.DEFAULT_PARAMETERS.samples = 10000. -
seconds: количество секунд, выделенное на процесс тестирования. По истечении этого времени испытание прекращается (независимо от значенияsamples), но всегда собирается как минимум одна проба. На практике фактическое время выполнения может превышать выделенное на величину длительности пробы. По умолчаниюBenchmarkTools.DEFAULT_PARAMETERS.seconds = 5. -
evals: количество вычислений на пробу. Для достижения наилучших результатов это значение не должно меняться от одного испытания к другому. Приблизительное значение этого параметра для теста производительности можно автоматически определить с помощьюtune!, но использованиеtune!дает менее стабильный результат, чем установкаevalsвручную (автоматическая настройка при этом не производится). По умолчаниюBenchmarkTools.DEFAULT_PARAMETERS.evals = 1. Если исследуемая функция изменяет свои входные данные, скорее всего, лучше будет вручную задатьevals=1. -
overhead: расчетные циклические дополнительные затраты на одно вычисление в наносекундах, которые автоматически вычитаются из каждого измерения времени пробы. Значение по умолчанию —BenchmarkTools.DEFAULT_PARAMETERS.overhead = 0. Для определения этого значения эмпирическим путем можно вызвать функциюBenchmarkTools.estimate_overhead(при желании результат затем можно установить в качестве значения по умолчанию). -
gctrial: при значенииtrueперед выполнением данного испытания в рамках теста производительности выполняетсяgc(). По умолчаниюBenchmarkTools.DEFAULT_PARAMETERS.gctrial = true. -
gcsample: при значенииtrueперед каждой пробой выполняетсяgc(). По умолчаниюBenchmarkTools.DEFAULT_PARAMETERS.gcsample = false. -
time_tolerance: допустимый уровень шума для оценки времени в ходе теста, выраженный в процентах. Используется после выполнения теста производительности при анализе результатов. По умолчаниюBenchmarkTools.DEFAULT_PARAMETERS.time_tolerance = 0.05. -
memory_tolerance: допустимый уровень шума для оценки объема памяти в ходе теста, выраженный в процентах. Используется после выполнения теста производительности при анализе результатов. По умолчаниюBenchmarkTools.DEFAULT_PARAMETERS.memory_tolerance = 0.01.
Чтобы изменить значения по умолчанию для указанных выше полей, можно, например, изменить поля BenchmarkTools.DEFAULT_PARAMETERS:
# изменяем значение по умолчанию для `seconds` на 2.5
BenchmarkTools.DEFAULT_PARAMETERS.seconds = 2.50
# изменяем значение по умолчанию для `time_tolerance` на 0.20
BenchmarkTools.DEFAULT_PARAMETERS.time_tolerance = 0.20
Вот пример, демонстрирующий передачу этих параметров в определения тестов производительности:
b = @benchmarkable sin(1) seconds=1 time_tolerance=0.01
run(b) # равносильно run(b, seconds = 1, time_tolerance = 0.01)
Интерполяция значений в выражения теста производительности
Значения можно интерполировать в выражениях @benchmark и @benchmarkable:
# rand(1000) выполняется для каждого вычисления
julia> @benchmark sum(rand(1000))
BenchmarkTools.Trial: 10000 samples with 10 evaluations.
Range (min … max): 1.153 μs … 142.253 μs ┊ GC (min … max): 0.00% … 96.43%
Time (median): 1.363 μs ┊ GC (median): 0.00%
Time (mean ± σ): 1.786 μs ± 4.612 μs ┊ GC (mean ± σ): 9.58% ± 3.70%
▄▆██▇▇▆▄▃▂▁ ▁▁▂▂▂▂▂▂▂▁▂▁
████████████████▆▆▇▅▆▇▆▆▆▇▆▇▆▆▅▄▄▄▅▃▄▇██████████████▇▇▇▇▆▆▇▆▆▅▅▅▅
1.15 μs Histogram: log(frequency) by time 3.8 μs (top 1%)
Memory estimate: 7.94 KiB, allocs estimate: 1.
# rand(1000) вычисляется во время определения, и получившееся
# значение интерполируется в выражение теста производительности
julia> @benchmark sum($(rand(1000)))
BenchmarkTools.Trial: 10000 samples with 963 evaluations.
Range (min … max): 84.477 ns … 241.602 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 84.497 ns ┊ GC (median): 0.00%
Time (mean ± σ): 85.125 ns ± 5.262 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
█
█▅▇▅▄███▇▇▆▆▆▄▄▅▅▄▄▅▄▄▅▄▄▄▄▁▃▄▁▁▃▃▃▄▃▁▃▁▁▁▁▁▃▁▁▁▁▁▁▁▁▁▁▃▃▁▁▁▃▁▁▁▁▆
84.5 ns Histogram: log(frequency) by time 109 ns (top 1%)
Memory estimate: 0 bytes, allocs estimate: 0.
Желательно придерживаться того правила, что внешние переменные должны явно интерполироваться в выражение теста производительности:
julia> A = rand(1000);
# НЕПРАВИЛЬНО: в контексте теста производительности переменная А является глобальной
julia> @benchmark [i*i for i in A]
BenchmarkTools.Trial: 10000 samples with 54 evaluations.
Range (min … max): 889.241 ns … 29.584 μs ┊ GC (min … max): 0.00% … 93.33%
Time (median): 1.073 μs ┊ GC (median): 0.00%
Time (mean ± σ): 1.296 μs ± 2.004 μs ┊ GC (mean ± σ): 14.31% ± 8.76%
▃█▆
▂▂▄▆███▇▄▄▃▃▃▃▃▂▂▂▂▂▂▂▂▂▂▂▁▂▂▂▁▂▂▁▁▁▁▁▂▁▁▁▁▂▂▁▁▁▁▂▁▁▁▁▁▁▂▂▂▂▂▂▂▂▂▂
889 ns Histogram: frequency by time 2.92 μs (top 1%)
Memory estimate: 7.95 KiB, allocs estimate: 2.
# ПРАВИЛЬНО: в контексте теста производительности переменная А является константой
julia> @benchmark [i*i for i in $A]
BenchmarkTools.Trial: 10000 samples with 121 evaluations.
Range (min … max): 742.455 ns … 11.846 μs ┊ GC (min … max): 0.00% … 88.05%
Time (median): 909.959 ns ┊ GC (median): 0.00%
Time (mean ± σ): 1.135 μs ± 1.366 μs ┊ GC (mean ± σ): 16.94% ± 12.58%
▇█▅▂ ▁
████▇▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄▅▆██
742 ns Histogram: log(frequency) by time 10.3 μs (top 1%)
Memory estimate: 7.94 KiB, allocs estimate: 1.
(Обратите внимание, что «КиБ» — это сокращение СИ для кибибайта: 1024 байта.)
Имейте в виду, что внешнее состояние можно изменять непосредственно из теста производительности:
julia> A = zeros(3);
# при каждом вычислении изменяется A
julia> b = @benchmarkable fill!($A, rand());
julia> run(b, samples = 1);
julia> A
3-element Vector{Float64}:
0.4615582142515109
0.4615582142515109
0.4615582142515109
julia> run(b, samples = 1);
julia> A
3-element Vector{Float64}:
0.06373849439691504
0.06373849439691504
0.06373849439691504
Обычно в @benchmark или @benchmarkable нельзя использовать переменные с локальной областью, так как все тесты производительности изначально определены в области верхнего уровня. Однако это ограничение можно обойти, интерполировав локальные переменные в выражение теста производительности:
# выдаст ошибку UndefVar для `x`
julia> let x = 1
@benchmark sin(x)
end
# будет работать
julia> let x = 1
@benchmark sin($x)
end
Этапы подготовки и завершения
Пакет BenchmarkTools позволяет передавать выражения setup и teardown в @benchmark и @benchmarkable. Выражение setup вычисляется непосредственно перед выполнением пробы, а выражение teardown — сразу после выполнения пробы. Вот пример того, где это может быть полезно:
julia> x = rand(100000);
# Для каждой пробы привязываем переменную `y` к новой копии `x`. Как вы
# можете видеть, переменная `y` доступна в области основного выражения.
julia> b = @benchmarkable sort!(y) setup=(y = copy($x))
Benchmark(evals=1, seconds=5.0, samples=10000)
julia> run(b)
BenchmarkTools.Trial: 819 samples with 1 evaluations.
Range (min … max): 5.983 ms … 6.954 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 6.019 ms ┊ GC (median): 0.00%
Time (mean ± σ): 6.029 ms ± 46.222 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
▃▂▂▄█▄▂▃
▂▃▃▄▆▅████████▇▆▆▅▄▄▄▅▆▄▃▄▅▄▃▂▃▃▃▂▂▃▁▂▂▂▁▂▂▂▂▂▂▁▁▁▁▂▂▁▁▁▂▂▁▁▂▁▁▂
5.98 ms Histogram: frequency by time 6.18 ms (top 1%)
Memory estimate: 0 bytes, allocs estimate: 0.
В приведенном выше примере мы тестируем производительность метода сортировки на месте в Julia. Без этапа подготовки нам пришлось бы либо выделять новый входной вектор для каждой пробы (что исказило бы результаты из-за времени, затрачиваемого на выделение), либо использовать один и тот же входной вектор для каждой пробы (из-за чего для всех проб, кроме первой, тестировалось бы не то, что нужно, а именно сортировка уже отсортированного вектора). Этап подготовки решает эту проблему, позволяя выполнить некоторые действия, результат которых может использоваться в основном выражении, но не влияет на результаты оценки производительности.
Обратите внимание, что этапы setup и teardown выполняются для каждой пробы, а не каждого вычисления. Поэтому в приведенном выше примере сортировки при evals/sample > 1 не были бы получены желаемые результаты (из-за той же проблемы тестирования на уже отсортированном векторе).
Если подготавливается несколько объектов, выражения присваивания должны разделяться точками с запятой следующим образом:
julia> @btime x + y setup = (x=1; y=2) # работает
1.238 ns (0 allocations: 0 bytes)
3
julia> @btime x + y setup = (x=1, y=2) # выдает ошибку
ERROR: UndefVarError: `x` not defined
В этом также кроется причина ошибки, которая возникает, если поставить запятую при подготовке с единственным аргументом:
julia> @btime exp(x) setup = (x=1,) # выдает ошибку
ERROR: UndefVarError: `x` not defined
Сведения об оптимизациях компилятора
LLVM и компилятор Julia могут выполнять оптимизации выражений @benchmarkable. В некоторых случаях эти оптимизации позволяют полностью исключить вычисления, что приводит к неожиданно «быстрым» тестам производительности. Например, следующее выражение не выделяет ресурсов:
julia> @benchmark (view(a, 1:2, 1:2); 1) setup=(a = rand(3, 3))
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
Range (min … max): 2.885 ns … 14.797 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 2.895 ns ┊ GC (median): 0.00%
Time (mean ± σ): 3.320 ns ± 0.909 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
█ ▁ ▁ ▁▁▁ ▂▃▃▁
█▁▁▇█▇▆█▇████████████████▇█▇█▇▇▇▇█▇█▇▅▅▄▁▁▁▁▄▃▁▃▃▁▄▃▁▄▁▃▅▅██████
2.88 ns Histogram: log(frequency) by time 5.79 ns (top 1%)
Memory estimate: 0 bytes, allocs estimate: 0.0
Однако это не значит, что ресурсы не будут выделяться при вызове view(a, 1:2, 1:2):
julia> @benchmark view(a, 1:2, 1:2) setup=(a = rand(3, 3))
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
Range (min … max): 3.175 ns … 18.314 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 3.176 ns ┊ GC (median): 0.00%
Time (mean ± σ): 3.262 ns ± 0.882 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
█
█▁▂▁▁▁▂▁▂▁▂▁▁▂▁▁▂▂▂▂▂▂▁▁▂▁▁▂▁▁▁▂▂▁▁▁▂▁▂▂▁▂▁▁▂▂▂▁▂▂▂▂▂▂▂▂▂▂▂▁▂▂▁▂
3.18 ns Histogram: frequency by time 4.78 ns (top 1%)
Memory estimate: 0 bytes, allocs estimate: 0.8
Ключевым здесь является то, что эти два теста производительности измеряют разное, несмотря на схожесть кода. В первом примере Julia удалось исключить view(a, 1:2, 1:2) при оптимизации, так как удалось доказать, что значение не возвращается и a не изменяется. Во втором примере оптимизация не выполняется, поскольку view(a, 1:2, 1:2) является возвращаемым значением выражения теста производительности.
BenchmarkTools точно сообщает производительность предоставленного кода, включая все оптимизации компилятора, которые могут полностью исключать этот код. Задача разработки таких тестов производительности, которые действительно выполняют нужный код, лежит на вас.
Одна из распространенных ситуаций, когда из-за оптимизатора Julia тест производительности может измерять не то, на что рассчитывал пользователь, — это простые операции, в которых все значения известны во время компиляции. Предположим, вы хотите измерить время, необходимое для сложения двух целых чисел:
julia> a = 1; b = 2
2
julia> @btime $a + $b
0.024 ns (0 allocations: 0 bytes)
3
В данном случае исходя из свойств операции +(::Int, ::Int) компилятор Julia определил, что можно безопасно заменить $a + $b на 3 во время компиляции. Предотвратить такое поведение оптимизатора можно путем именования и разыменовывая интерполированных переменных.
julia> @btime $(Ref(a))[] + $(Ref(b))[]
1.277 ns (0 allocations: 0 bytes)
3
Обработка результатов теста производительности
BenchmarkTools предоставляет четыре типа, связанных с результатами тестов производительности:
-
Trial: хранит все пробы, полученные в ходе испытания производительности, а также параметры этого испытания -
TrialEstimate: одиночная оценка, используемая для обобщенияTrial -
TrialRatio: сравнение двухTrialEstimate -
TrialJudgement: классификация полейTrialRatioкакinvariant,regressionилиimprovement
В этом разделе приведено лишь несколько примеров, демонстрирующих эти типы. Полный список поддерживаемых функций см. в справочном документе.
Trial и TrialEstimate
В результате выполнения теста производительности создается экземпляр типа Trial:
julia> t = @benchmark eigen(rand(10, 10))
BenchmarkTools.Trial: 10000 samples with 1 evaluations.
Range (min … max): 26.549 μs … 1.503 ms ┊ GC (min … max): 0.00% … 93.21%
Time (median): 30.818 μs ┊ GC (median): 0.00%
Time (mean ± σ): 31.777 μs ± 25.161 μs ┊ GC (mean ± σ): 1.31% ± 1.63%
▂▃▅▆█▇▇▆▆▄▄▃▁▁
▁▁▁▁▁▁▂▃▄▆████████████████▆▆▅▅▄▄▃▃▃▂▂▂▂▂▂▁▂▁▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
26.5 μs Histogram: frequency by time 41.3 μs (top 1%)
Memory estimate: 16.36 KiB, allocs estimate: 19.
julia> dump(t) # вот что фактически хранится в Trial
BenchmarkTools.Trial
params: BenchmarkTools.Parameters
seconds: Float64 5.0
samples: Int64 10000
evals: Int64 1
overhead: Float64 0.0
gctrial: Bool true
gcsample: Bool false
time_tolerance: Float64 0.05
memory_tolerance: Float64 0.01
times: Array{Float64}((10000,)) [26549.0, 26960.0, 27030.0, 27171.0, 27211.0, 27261.0, 27270.0, 27311.0, 27311.0, 27321.0 … 55383.0, 55934.0, 58649.0, 62847.0, 68547.0, 75761.0, 247081.0, 1.421718e6, 1.488322e6, 1.50329e6]
gctimes: Array{Float64}((10000,)) [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 … 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.366184e6, 1.389518e6, 1.40116e6]
memory: Int64 16752
allocs: Int64 19
Как видно в приведенном выше примере, с помощью Trial несколько оценок времени выводятся в отформатированном виде. Эти оценки можно рассчитать самостоятельно, используя функции minimum, maximum, median, mean и std (обратите внимание, что median, mean и std повторно экспортируются в BenchmarkTools из Statistics):
julia> minimum(t)
BenchmarkTools.TrialEstimate:
time: 26.549 μs
gctime: 0.000 ns (0.00%)
memory: 16.36 KiB
allocs: 19
julia> maximum(t)
BenchmarkTools.TrialEstimate:
time: 1.503 ms
gctime: 1.401 ms (93.21%)
memory: 16.36 KiB
allocs: 19
julia> median(t)
BenchmarkTools.TrialEstimate:
time: 30.818 μs
gctime: 0.000 ns (0.00%)
memory: 16.36 KiB
allocs: 19
julia> mean(t)
BenchmarkTools.TrialEstimate:
time: 31.777 μs
gctime: 415.686 ns (1.31%)
memory: 16.36 KiB
allocs: 19
julia> std(t)
BenchmarkTools.TrialEstimate:
time: 25.161 μs
gctime: 23.999 μs (95.38%)
memory: 16.36 KiB
allocs: 19
Какое средство оценки следует использовать
Временные распределения во всех испытанных нами тестах производительности всегда скошены вправо. Это явление можно объяснить тем, что машинный шум, влияющий на процесс тестирования производительности, в некотором смысле является положительным — в действительности нет источников шума, которые регулярно заставляли бы ваш компьютер выполнять последовательность инструкций быстрее, чем теоретическое «идеальное» время, диктуемое вашим оборудованием. Согласно такой характеристике шума при тестировании производительности мы можем описать поведение средств оценки:
-
Минимум является надежной оценкой параметра расположения временного распределения и не должен рассматриваться как выброс.
-
Медиана, как надежная мера центральной тенденции, не должна быть существенно подвержена влиянию выбросов.
-
Среднее значение, как ненадежная мера центральной тенденции, обычно имеет положительную асимметрию из-за выбросов.
-
Максимальное значение следует рассматривать как преимущественно обусловленный шумом выброс и может резко меняться в ходе испытаний производительности.
TrialRatio и TrialJudgement
Пакет BenchmarkTools предоставляет функцию ratio для сравнения двух значений:
julia> ratio(3, 2)
1.5
julia> ratio(1, 0)
Inf
julia> ratio(0, 1)
0.0
# a == b — это особый случай, который дает 1.0 для предотвращения значений NaN
julia> ratio(0, 0)
1.0
При вызове функции ratio для двух экземпляров TrialEstimate сравниваются их поля:
julia> using BenchmarkTools
julia> b = @benchmarkable eigen(rand(10, 10));
julia> tune!(b);
julia> m1 = median(run(b))
BenchmarkTools.TrialEstimate:
time: 38.638 μs
gctime: 0.000 ns (0.00%)
memory: 9.30 KiB
allocs: 28
julia> m2 = median(run(b))
BenchmarkTools.TrialEstimate:
time: 38.723 μs
gctime: 0.000 ns (0.00%)
memory: 9.30 KiB
allocs: 28
julia> ratio(m1, m2)
BenchmarkTools.TrialRatio:
time: 0.997792009916587
gctime: 1.0
memory: 1.0
allocs: 1.0
Используйте функцию judge для определения того, представляет ли оценка, переданная в качестве первого аргумента, регрессию относительно второй оценки:
julia> m1 = median(@benchmark eigen(rand(10, 10)))
BenchmarkTools.TrialEstimate:
time: 38.745 μs
gctime: 0.000 ns (0.00%)
memory: 9.30 KiB
allocs: 28
julia> m2 = median(@benchmark eigen(rand(10, 10)))
BenchmarkTools.TrialEstimate:
time: 38.611 μs
gctime: 0.000 ns (0.00%)
memory: 9.30 KiB
allocs: 28
# процентное изменение находится в пределах допустимого уровня шума для всех полей
julia> judge(m1, m2)
BenchmarkTools.TrialJudgement:
time: +0.35% => invariant (5.00% tolerance)
memory: +0.00% => invariant (1.00% tolerance)
# при изменении параметра time_tolerance он помечается как регрессия
julia> judge(m1, m2; time_tolerance = 0.0001)
BenchmarkTools.TrialJudgement:
time: +0.35% => regression (0.01% tolerance)
memory: +0.00% => invariant (1.00% tolerance)
# меняем местами m1 и m2; в таком случае разница представляет собой улучшение
julia> judge(m2, m1; time_tolerance = 0.0001)
BenchmarkTools.TrialJudgement:
time: -0.35% => improvement (0.01% tolerance)
memory: +0.00% => invariant (1.00% tolerance)
# можно также передать TrialRatios
julia> judge(ratio(m1, m2)) == judge(m1, m2)
true
Обратите внимание, что изменения времени сборки мусора и счетчика выделенных ресурсов не классифицируются функцией judge. Это объясняется тем, что хотя время сборки мусора и счетчик выделенных ресурсов иногда и полезны для ответа на вопрос, почему произошла регрессия, как правило, бесполезны для определения того, имела ли она место. Вместо этого для определения того, является ли изменение кода улучшением или регрессией, как правило, учитываются только различия во времени и использовании памяти. Например, в маловероятном случае, когда изменение кода уменьшает время и использование памяти, но увеличивает время сборки мусора и количество выделенной памяти, такое изменение большинством будет сочтено улучшением. Верно и обратное: увеличение времени и объема используемой памяти будет считаться регрессией независимо от того, насколько уменьшается время сборки мусора или количество выделенной памяти.
Тип BenchmarkGroup
В реальных условиях зачастую приходится иметь дело с целыми пакетами тестов производительности, а не отдельными тестами. Тип BenchmarkGroup служит «организационной единицей» таких пакетов и может применяться для хранения и структурирования определений тестов производительности, необработанных данных Trial, результатов оценки и даже других экземпляров BenchmarkGroup.
Определение пакетов тестов производительности
В BenchmarkGroup хранится словарь Dict, сопоставляющий идентификаторы тестов производительности со значениями, а также описательные «теги», которые можно использовать для фильтрации группы по теме. Для начала давайте продемонстрируем, как можно использовать тип BenchmarkGroup для определения простого пакета тестов производительности:
# Определяем родительский объект BenchmarkGroup, который будет содержать пакет
suite = BenchmarkGroup()
# Добавляем в пакет тестов производительности несколько дочерних групп. Наиболее подходящим конструктором BenchmarkGroup
# в этом случае будет BenchmarkGroup(tags::Vector). Эти теги полезны
# для фильтрации тестов производительности по темам, о чем пойдет речь далее.
suite["utf8"] = BenchmarkGroup(["string", "unicode"])
suite["trig"] = BenchmarkGroup(["math", "triangles"])
# Добавляем тесты производительности в группу utf8
teststr = join(rand('a':'d', 10^4));
suite["utf8"]["replace"] = @benchmarkable replace($teststr, "a" => "b")
suite["utf8"]["join"] = @benchmarkable join($teststr, $teststr)
# Добавляем тесты производительности в группу trig
for f in (sin, cos, tan)
for x in (0.0, pi)
suite["trig"][string(f), x] = @benchmarkable $(f)($x)
end
end
Рассмотрим созданный пакет в REPL:
julia> suite
2-element BenchmarkTools.BenchmarkGroup:
tags: []
"utf8" => 2-element BenchmarkTools.BenchmarkGroup:
tags: ["string", "unicode"]
"join" => Benchmark(evals=1, seconds=5.0, samples=10000)
"replace" => Benchmark(evals=1, seconds=5.0, samples=10000)
"trig" => 6-element BenchmarkTools.BenchmarkGroup:
tags: ["math", "triangles"]
("cos", 0.0) => Benchmark(evals=1, seconds=5.0, samples=10000)
("sin", π = 3.1415926535897...) => Benchmark(evals=1, seconds=5.0, samples=10000)
("tan", π = 3.1415926535897...) => Benchmark(evals=1, seconds=5.0, samples=10000)
("cos", π = 3.1415926535897...) => Benchmark(evals=1, seconds=5.0, samples=10000)
("sin", 0.0) => Benchmark(evals=1, seconds=5.0, samples=10000)
("tan", 0.0) => Benchmark(evals=1, seconds=5.0, samples=10000)
Как и следовало ожидать, BenchmarkGroup поддерживает часть интерфейса Julia Associative. Полный список поддерживаемых функций можно найти в справочном документе.
Вложенный объект BenchmarkGroup можно также создать просто путем обращения к ключам по индексам:
suite2 = BenchmarkGroup()
suite2["my"]["nested"]["benchmark"] = @benchmarkable sum(randn(32))
В результате получается иерархический тест производительности, причем нет необходимости самостоятельно создавать BenchmarkGroup на каждом уровне.
Обратите внимание, что даже если ключ не существует, при обращении он создается автоматически. Таким образом, если вы хотите очистить неиспользуемые ключи, это можно сделать с помощью clear_empty!(suite).
Настройка и запуск BenchmarkGroup
Как и в случае с отдельными тестами производительности, можно использовать tune! и run применительно к целым экземплярам BenchmarkGroup (в продолжение предыдущего раздела):
# выполняем `tune!` для каждого теста производительности в `suite`
julia> tune!(suite);
# выполняем с предельным временем ~1 секунда на тест производительности
julia> results = run(suite, verbose = true, seconds = 1)
(1/2) benchmarking "utf8"...
(1/2) benchmarking "join"...
done (took 1.15406904 seconds)
(2/2) benchmarking "replace"...
done (took 0.47660775 seconds)
done (took 1.697970114 seconds)
(2/2) benchmarking "trig"...
(1/6) benchmarking ("tan",π = 3.1415926535897...)...
done (took 0.371586549 seconds)
(2/6) benchmarking ("cos",0.0)...
done (took 0.284178292 seconds)
(3/6) benchmarking ("cos",π = 3.1415926535897...)...
done (took 0.338527685 seconds)
(4/6) benchmarking ("sin",π = 3.1415926535897...)...
done (took 0.345329397 seconds)
(5/6) benchmarking ("sin",0.0)...
done (took 0.309887335 seconds)
(6/6) benchmarking ("tan",0.0)...
done (took 0.320894744 seconds)
done (took 2.022673065 seconds)
BenchmarkTools.BenchmarkGroup:
tags: []
"utf8" => BenchmarkGroup(["string", "unicode"])
"trig" => BenchmarkGroup(["math", "triangles"])
Работа с данными испытаний в BenchmarkGroup
Как видно из предыдущего раздела, при запуске пакета тестов производительности возвращается объект BenchmarkGroup, в котором хранятся данные Trial, а не сами тесты производительности:
julia> results["utf8"]
BenchmarkTools.BenchmarkGroup:
tags: ["string", "unicode"]
"join" => Trial(133.84 ms) # summary(::Trial) отображает оценочное минимальное время
"replace" => Trial(202.3 μs)
julia> results["trig"]
BenchmarkTools.BenchmarkGroup:
tags: ["math", "triangles"]
("tan",π = 3.1415926535897...) => Trial(28.0 ns)
("cos",0.0) => Trial(6.0 ns)
("cos",π = 3.1415926535897...) => Trial(22.0 ns)
("sin",π = 3.1415926535897...) => Trial(21.0 ns)
("sin",0.0) => Trial(6.0 ns)
("tan",0.0) => Trial(6.0 ns)
Большинство функций, применяемых к связанным с результатами типам (Trial, TrialEstimate, TrialRatio и TrialJudgement), также работают с BenchmarkGroup. Как правило, эти функции просто выполняют сопоставление со значениями групп:
julia> m1 = median(results["utf8"]) # == median(results["utf8"])
BenchmarkTools.BenchmarkGroup:
tags: ["string", "unicode"]
"join" => TrialEstimate(143.68 ms)
"replace" => TrialEstimate(203.24 μs)
julia> m2 = median(run(suite["utf8"]))
BenchmarkTools.BenchmarkGroup:
tags: ["string", "unicode"]
"join" => TrialEstimate(144.79 ms)
"replace" => TrialEstimate(202.49 μs)
julia> judge(m1, m2; time_tolerance = 0.001) # используем допуск по времени 0,1 %
BenchmarkTools.BenchmarkGroup:
tags: ["string", "unicode"]
"join" => TrialJudgement(-0.76% => improvement)
"replace" => TrialJudgement(+0.37% => regression)
Обращение по индексам к BenchmarkGroup с помощью @tagged
Иногда, особенно в случае с большими пакетами тестов производительности, возникает необходимость в фильтрации тестов по темам без учета структуры «ключ-значение» пакета. Например, может потребоваться запустить все тесты производительности, связанные со строками, даже если они относятся к различным группам или подгруппам. Для решения этой проблемы в типе BenchmarkGroup предусмотрена система тегов.
Рассмотрим следующий объект BenchmarkGroup, содержащий несколько вложенных дочерних групп, каждая из которых имеет собственные теги:
julia> g = BenchmarkGroup([], # родительская группа без тегов
"c" => BenchmarkGroup(["5", "6", "7"]), # теги 5, 6, 7
"b" => BenchmarkGroup(["3", "4", "5"]), # теги 3, 4, 5
"a" => BenchmarkGroup(["1", "2", "3"], # содержит теги и дочерние группы
"d" => BenchmarkGroup(["8"], 1 => 1),
"e" => BenchmarkGroup(["9"], 2 => 2)));
julia> g
BenchmarkTools.BenchmarkGroup:
tags: []
"c" => BenchmarkTools.BenchmarkGroup:
tags: ["5", "6", "7"]
"b" => BenchmarkTools.BenchmarkGroup:
tags: ["3", "4", "5"]
"a" => BenchmarkTools.BenchmarkGroup:
tags: ["1", "2", "3"]
"e" => BenchmarkTools.BenchmarkGroup:
tags: ["9"]
2 => 2
"d" => BenchmarkTools.BenchmarkGroup:
tags: ["8"]
1 => 1
Эту группу можно отфильтровать по тегу, используя макрос @tagged. Этот макрос принимает специальный предикат и возвращает объект, который можно использовать для обращения по индексам к BenchmarkGroup. Например, можно выбрать все группы, помеченные как "3" или "7", но не как "1":
julia> g[@tagged ("3" || "7") && !("1")]
BenchmarkTools.BenchmarkGroup:
tags: []
"c" => BenchmarkGroup(["5", "6", "7"])
"b" => BenchmarkGroup(["3", "4", "5"])
Как видно из примера, допустимый синтаксис для предиката @tagged включает !, (), ||, &&, помимо самих тегов. Макрос @tagged заменяет каждый тег в выражении предиката проверкой наличия у группы указанного тега, возвращая true в случае его наличия и false в противном случае. Группа g считается имеющей заданный тег t в следующих случаях:
-
Тег
tявным образом добавляется кgпри создании (например,g = BenchmarkGroup([t])). -
Тег
tпредставляет собой ключ, указывающий наgв родительской группеg(например,BenchmarkGroup([], t => g)). -
t— это тег одной из родительских группg(любой из вышестоящих вплоть до корневой группы).
Проиллюстрируем два последних условия:
# можно было бы также использовать `@tagged "1"`, `@tagged "a"`, `@tagged "e" || "d"`
julia> g[@tagged "8" || "9"]
BenchmarkTools.BenchmarkGroup:
tags: []
"a" => BenchmarkTools.BenchmarkGroup:
tags: ["1", "2", "3"]
"e" => BenchmarkTools.BenchmarkGroup:
tags: ["9"]
2 => 2
"d" => BenchmarkTools.BenchmarkGroup:
tags: ["8"]
1 => 1
julia> g[@tagged "d"]
BenchmarkTools.BenchmarkGroup:
tags: []
"a" => BenchmarkTools.BenchmarkGroup:
tags: ["1", "2", "3"]
"d" => BenchmarkTools.BenchmarkGroup:
tags: ["8"]
1 => 1
julia> g[@tagged "9"]
BenchmarkTools.BenchmarkGroup:
tags: []
"a" => BenchmarkTools.BenchmarkGroup:
tags: ["1", "2", "3"]
"e" => BenchmarkTools.BenchmarkGroup:
tags: ["9"]
2 => 2
Обращение по индексам к BenchmarkGroup с помощью другого объекта BenchmarkGroup
Иногда бывает полезно создать объект BenchmarkGroup с ключами из одного объекта BenchmarkGroup, а значениями из другого. Для этого можно использовать обращение по индексам ко второму объекту BenchmarkGroup с помощью первого:
julia> g # значения конечных элементов целочисленные
BenchmarkTools.BenchmarkGroup:
tags: []
"c" => BenchmarkTools.BenchmarkGroup:
tags: []
"1" => 1
"2" => 2
"3" => 3
"b" => BenchmarkTools.BenchmarkGroup:
tags: []
"1" => 1
"2" => 2
"3" => 3
"a" => BenchmarkTools.BenchmarkGroup:
tags: []
"1" => 1
"2" => 2
"3" => 3
"d" => BenchmarkTools.BenchmarkGroup:
tags: []
"1" => 1
"2" => 2
"3" => 3
julia> x # обратите внимание, что значения конечных элементов — символы
BenchmarkTools.BenchmarkGroup:
tags: []
"c" => BenchmarkTools.BenchmarkGroup:
tags: []
"2" => '2'
"a" => BenchmarkTools.BenchmarkGroup:
tags: []
"1" => '1'
"3" => '3'
"d" => BenchmarkTools.BenchmarkGroup:
tags: []
"1" => '1'
"2" => '2'
"3" => '3'
julia> g[x] # обращаемся по индексам к `g` с помощью ключей `x`
BenchmarkTools.BenchmarkGroup:
tags: []
"c" => BenchmarkTools.BenchmarkGroup:
tags: []
"2" => 2
"a" => BenchmarkTools.BenchmarkGroup:
tags: []
"1" => 1
"3" => 3
"d" => BenchmarkTools.BenchmarkGroup:
tags: []
"1" => 1
"2" => 2
"3" => 3
Вот пример ситуации, в которой это может быть полезно: имеется пакет тестов производительности и соответствующая группа TrialJudgement, и необходимо повторно выполнить те тесты из пакета, которые считаются регрессионными в группе оценочных тестов. Это можно легко сделать с помощью следующего кода:
run(suite[regressions(judgements)])
Обращение по индексам к BenchmarkGroup с помощью Vector
Возможно, вы заметили, что вложенные экземпляры BenchmarkGroup образуют древовидную структуру, в которой корневым узлом является родительская группа, промежуточными узлами — дочерние группы, а конечные элементы принимают такие значения, как данные испытаний и определения тестов производительности.
Поскольку такие деревья могут быть произвольно асимметричными, реализация некоторых преобразований BenchmarkGroup с использованием только ранее обсуждавшихся способов индексирования может быть затруднительной.
Для решения этой проблемы BenchmarkTools позволяет обращаться к узлам группы по уникальным индексам, используя Vector родительских ключей узлов. Например:
julia> g = BenchmarkGroup([], 1 => BenchmarkGroup([], "a" => BenchmarkGroup([], :b => 1234)));
julia> g
BenchmarkTools.BenchmarkGroup:
tags: []
1 => BenchmarkTools.BenchmarkGroup:
tags: []
"a" => BenchmarkTools.BenchmarkGroup:
tags: []
:b => 1234
julia> g[[1]] # == g[1]
BenchmarkTools.BenchmarkGroup:
tags: []
"a" => BenchmarkTools.BenchmarkGroup:
tags: []
:b => 1234
julia> g[[1, "a"]] # == g[1]["a"]
BenchmarkTools.BenchmarkGroup:
tags: []
:b => 1234
julia> g[[1, "a", :b]] # == g[1]["a"][:b]
1234
Обратите внимание, что эта схема индексирования также работает с setindex!:
julia> g[[1, "a", :b]] = "hello"
"hello"
julia> g
BenchmarkTools.BenchmarkGroup:
tags: []
1 => BenchmarkTools.BenchmarkGroup:
tags: []
"a" => BenchmarkTools.BenchmarkGroup:
tags: []
:b => "hello"
При присвоении значений элементам BenchmarkGroup с помощью Vector создаются необходимые подгруппы:
julia> g[[2, "a", :b]] = "hello again"
"hello again"
julia> g
2-element BenchmarkTools.BenchmarkGroup:
tags: []
2 => 1-element BenchmarkTools.BenchmarkGroup:
tags: []
"a" => 1-element BenchmarkTools.BenchmarkGroup:
tags: []
:b => "hello again"
1 => 1-element BenchmarkTools.BenchmarkGroup:
tags: []
"a" => 1-element BenchmarkTools.BenchmarkGroup:
tags: []
:b => "hello"
С помощью функции leaves можно создать итератор по парам «индекс-значение» конечных элементов группы:
julia> g = BenchmarkGroup(["1"],
"2" => BenchmarkGroup(["3"], 1 => 1),
4 => BenchmarkGroup(["3"], 5 => 6),
7 => 8,
9 => BenchmarkGroup(["2"],
10 => BenchmarkGroup(["3"]),
11 => BenchmarkGroup()));
julia> collect(leaves(g))
3-element Array{Any,1}:
([7],8)
([4,5],6)
(["2",1],1)
Обратите внимание, что конечные узлы дочерней группы не рассматриваются функцией leaves как конечные элементы.
Кэширование Parameters
В BenchmarkTools обычно используется следующий рабочий процесс:
-
Начинаем сеанс Julia.
-
Выполняем пакет тестов производительности, используя старую версию пакета.
old_results = run(suite, verbose = true)
```
4. Каким-либо образом сохраняем результаты (например, в файле JSON).
julia BenchmarkTools.save("old_results.json", old_results)
4. Начинаем новый сеанс Julia. 5. Выполняем пакет тестов производительности, используя новую версию пакета. ```julia results = run(suite, verbose = true)
-
Сравниваем новые результаты с результатами, сохраненными на этапе 3, чтобы определить статус регрессии.
julia old_results = BenchmarkTools.load("old_results.json") BenchmarkTools.judge(minimum(results), minimum(old_results))
В этом рабочем процессе есть несколько проблем, и все они связаны с настройкой параметров (которая происходит на этапах 2 и 5):
-
Согласованность: при достаточном количестве времени последовательные вызовы функции
tune!обычно дают достаточно стабильные значения для параметра «количество вычислений на пробу», даже несмотря на шум. Однако некоторые тесты производительности очень чувствительны к незначительным изменениям этого параметра. Поэтому желательно иметь некоторую гарантию того, что все эксперименты настроены одинаково (то есть гарантию того, что на этапе 2 будут использоваться точно такие же параметры, как и на этапе 5). -
Время завершения: для большинства тестов производительности функции
tune!приходится выполнять множество вычислений, чтобы определить правильные параметры для конкретного теста, зачастую больше, чем при проведении испытания. Фактически большая часть общего времени, затрачиваемого на тестирование, обычно уходит на настройку параметров, а не на непосредственное проведение испытаний.
В BenchmarkTools эти проблемы решаются благодаря возможности предварительно настроить пакет тестов производительности, сохранить параметры «количества вычислений на пробу» и загружать их по запросу:
julia
# ненастроенный пакет тестов производительности
julia> suite BenchmarkTools.BenchmarkGroup: tags: [] "utf8" => BenchmarkGroup(["string", "unicode"]) "trig" => BenchmarkGroup(["math", "triangles"])
# настраиваем параметры тестов производительности в пакете
julia> tune!(suite);
# сохраняем параметры пакета, используя тонкую оболочку
# вокруг JSON (эта оболочка сохраняет совместимость
# с различными версиями BenchmarkTools)
julia> BenchmarkTools.save("params.json", params(suite));
Теперь, вместо того чтобы каждый раз настраивать suite при загрузке тестов производительности в новом сеансе Julia, мы можем просто загрузить параметры из файла JSON с помощью функции loadparams!. Индекс [1] в вызове load возвращает первое сериализованное значение в файле JSON. В данном случае это параметры.
julia
# синтаксис имеет вид loadparams!(group, paramsgroup, fields...)
julia> loadparams!(suite, BenchmarkTools.load("params.json")[1], :evals, :samples);
Такой способ кэширования параметров приводит к значительному сокращению времени выполнения и, что более важно, к гораздо более стабильным результатам.
Визуализация результатов тестов производительности
Для сравнения результатов двух или более тестов производительности можно вручную указать диапазон гистограммы, используя IOContext для задания значений :histmin и :histmax:
julia julia> io = IOContext(stdout, :histmin=>0.5, :histmax=>8, :logbins=>true) IOContext(Base.TTY(RawFD(13) open, 0 bytes waiting))
julia> b = @benchmark x^3 setup=(x = rand()); show(io, MIME("text/plain"), b) BenchmarkTools.Trial: 10000 samples with 1000 evaluations. Range (min … max): 1.239 ns … 31.433 ns ┊ GC (min … max): 0.00% … 0.00% Time (median): 1.244 ns ┊ GC (median): 0.00% Time (mean ± σ): 1.266 ns ± 0.611 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
█
▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▂ 0.5 ns Histogram: log(frequency) by time 8 ns <
Memory estimate: 0 bytes, allocs estimate: 0. julia> b = @benchmark x^3.0 setup=(x = rand()); show(io, MIME("text/plain"), b) BenchmarkTools.Trial: 10000 samples with 1000 evaluations. Range (min … max): 5.636 ns … 38.756 ns ┊ GC (min … max): 0.00% … 0.00% Time (median): 5.662 ns ┊ GC (median): 0.00% Time (mean ± σ): 5.767 ns ± 1.384 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
█▆ ▂ ▁ ```
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███▄▄▃█▁▁▁▁▁▁▁▁▁▁▁▁ █ 0.5 ns Histogram: log(frequency) by time 8 ns <
Memory estimate: 0 bytes, allocs estimate: 0.
Присвойте `:logbins` значение `true` или `false`, чтобы обеспечить одинаковое вертикальное масштабирование (логарифмическая частота или частота). Объект `Trial` можно визуализировать с помощью пакета `BenchmarkPlots`:
julia using BenchmarkPlots, StatsPlots b = @benchmarkable lu(rand(10,10)) t = run(b)
plot(t)
Результаты оценки времени отображаются в виде графика «скрипка». Можно использовать любые именованные аргументы из `Plots.jl`, например `st=:box` или `yaxis=:log10`. Если `BenchmarkGroup` содержит (только) объекты `Trial`, его результаты можно визуализировать так:
julia using BenchmarkPlots, StatsPlots t = run(g) plot(t) `
В результате каждый объект Trial отображается в виде графика «скрипка».
Различные советы и информация
-
BenchmarkTools ограничивает минимальное измеримое время выполнения теста производительности одной пикосекундой.
-
Если вы используете
randили иную подобную функцию для генерирования значений для тестов производительности, следует инициализировать генератор случайных чисел (или предоставить уже инициализированный генератор случайных чисел), чтобы значения между испытаниями, пробами или вычислениями были согласованными. -
В BenchmarkTools по возможности обеспечивается устойчивость к машинному шуму между пробами, но с машинным шумом между испытаниями сделать это практически невозможно. Чтобы уменьшить влияние такого шума, рекомендуется выделить ресурсы ЦП и памяти для процесса тестирования производительности в Julia с помощью инструмента экранирования, например cset.
-
На некоторых компьютерах и при использовании некоторых версий BLAS и Julia количество рабочих потоков BLAS может превышать количество доступных ядер. Иногда это может приводить к проблемам с планированием и нестабильной производительности при выполнении тестов, интенсивно использующих BLAS. Для решения этой проблемы можно использовать
BLAS.set_num_threads(i::Int)в REPL Julia для проверки того, что количество потоков BLAS не превышает количество доступных ядер. -
@benchmarkвычисляется в глобальной области даже при вызове из локальной.