В последнее время я часто видел обсуждения репозитория сравнения языков. В нём CRuby был третьим снизу, обгоняя по скорости только R и Python.
Автор репозитория @BenjDicken создал забавную визуализацию производительности каждого из языков. Вот одна из визуализаций, в которой по бенчмаркам Ruby оказывается третьим с конца:
Код этой визуализации взят с https://benjdd.com/languages/ с разрешения @BenjDicken
Репозиторий описывается так:
Репозиторий коллективной разработки небольших бенчмарков для сравнения языков.
В нём есть два бенчмарка:
-
Loops, делающий упор на циклы, условные операторы и простую математику.
-
Fibonacci, в котором в первую очередь учитываются лишние затраты на вызов функций и рекурсию.
Пример Loops итеративно выполняется 1 миллиард раз при помощи вложенного цикла:
u = ARGV[0].to_i r = rand(10_000) a = Array.new(10_000, 0) (0...10_000).each do |i| (0...100_000).each do |j| a[i] += j % u end a[i] += r end puts a[r]
Пример Fibonacci это простая «наивная» реализация вычисления чисел Фибоначчи1:
def fibonacci(n) return 0 if n == 0 return 1 if n == 1 fibonacci(n - 1) + fibonacci(n - 2) end u = ARGV[0].to_i r = 0 (1...u).each do |i| r += fibonacci(i) end puts r
Код на Ruby 3.3.6, выполнявшийся на M3 MacBook Pro @BenjDicken, потратил 29 секунд на пример Loops и 12 секунд на пример Fibonacci. Для сравнения: на выполнение обоих примеров у node.js ушло чуть больше секунды — не очень хорошие показатели для Ruby.
Fibonacci |
Loops |
|
---|---|---|
Ruby |
12,17 с |
28,80 с |
node.js |
1,11 с |
1,03 с |
В дальнейшем я будут опираться на бенчмарки, запускаемые на моём компьютере. Выполнив тот же бенчмарк на моём M2 MacBook Air, я получил 33,43 секунды для loops и 16,33 секунды для fibonacci — показатели ещё хуже. Node выполнял fibonacci чуть больше 1 секунды, а пример loops — 2 секунды.
Fibonacci |
Loops |
|
---|---|---|
Ruby |
16,33 с |
33,43 с |
node.js |
1,36 с |
2,07 с |
Кого это волнует?
По большей мере подобные бенчмарки бессмысленны. Python был самым медленным языком в бенчмарке, однако в то же время самым используемым языком Github на октябрь 2024 года. На Ruby написано одно из самых крупных веб-приложений в мире. Недавно я запускал бенчмарк производительности веб-сокетов между веб-сервером Ruby Falcon и node.js, и результаты Ruby были близки к результатам node.js. Вы чаще выполняете миллиард итераций цикла или используете веб-сокеты?
Язык программирования должен быть достаточно эффективным, за этим идёт полезность языка и тип ваших задач, и продуктивность языка перевешивает скорость выполнения миллиарда итераций цикла или намеренно выбранной неэффективной реализации метода Фибоначчи.
Тем не менее:
-
Мир программирования любит бенчмарки
-
Быстрый бенчмарк не имеет ценности на практике, но привлекает интерес людей к языку. Кто-то может сказать, что он упрощает масштабирование производительности, но об этом можно поспорить2
-
То, что выбранный вами язык показывает себя не с лучшей стороны, раздражает. Здорово, когда можно сказать: «Я пользуюсь и наслаждаюсь этим языком, и он быстро работает во всех бенчмарках!»
В случае с этим бенчмарком Ruby у меня было чувство, что в коде на Ruby не применили YJIT, поэтому я проверил репозиторий. Команда имела следующий вид:
ruby ./code.rb 40
Мы уже знаем мои результаты (33 секунды и 16 секунд). Что мы получим, если применим YJIT?
ruby --yjit ./code.rb 40
Fibonacci |
Loops |
|
---|---|---|
Ruby |
2,06 с |
25,57 с |
Отлично! С YJIT тест Fibonacci получает огромный прирост скорости — время выполнения упало с 16,88 секунды до 2,06 секунды. Скорость уже близка к node.js!
Однако в случае примера с циклом достижения YJIT гораздо более скромные — снижение составило с 33,43 секунды до 25,57 секунды. Почему?
Командные усилия
Не я один попробовал запускать эти примеры кода с YJIT. @bsilva96 задал в X те же вопросы:
@k0kubun рассуждал, почему всё было так медленно и как можно повысить производительность:
Давайте разберём его ответ. Он состоит из трёх частей:
-
Range#each
в Ruby 3.4 по-прежнему написан на C -
Integer#times
переписан с C на Ruby в Ruby 3.3 -
Array#each
переписан с C на Ruby в Ruby 3.4
1. Range#each по-прежнему написан на C, который YJIT не может оптимизировать
Вернёмся к коду на Ruby:
(0...10_000).each do |i| (0...100_000).each do |j| a[i] += j % u end a[i] += r end
Он написан как range, а range имеет собственную реализацию each
, которая, очевидно, написана на C. В кодовой базе CRuby относительно легко ориентироваться. Давайте найдём эту реализацию.
У большинства базовых классов в Ruby есть файлы верхнего уровня на C, названные по их именам: в данном случае у нас имеется range.c
в корне проекта. У CRuby есть достаточно удобочитаемый интерфейс для раскрытия функций на C в виде классов и методов: существует функция Init
, обычно находящаяся внизу файла. Внутри этой Init
классы, модули и методы раскрываются из C в Ruby. Вот соответствующие части Init_Range
:
void Init_Range(void) { //... rb_cRange = rb_struct_define_without_accessor( "Range", rb_cObject, range_alloc, "begin", "end", "excl", NULL); rb_include_module(rb_cRange, rb_mEnumerable); // ... rb_define_method(rb_cRange, "each", range_each, 0);
Сначала мы определяем класс Range
при помощи rb_struct_define...
. Мы назвали его Range
с суперклассом Object
(rb_cObject
), и параметрами инициализации (begin
, end
и параметром, определяющим, нужно ли исключать последнее значение, например, синтаксис интервала ..
или ...
).
Далее мы включаем Enumerable
при помощи rb_include_module
. Это даёт нам доступ ко всем замечательным методам перечислений Ruby наподобие map
, select
, include?
и кучи других. Достаточно предоставить реализацию each
, и она займётся всем остальным.
Затем мы определяем метод each
. Он реализован функцией range_each
на C и получает ноль явных аргументов (блоки в этом количестве не учитываются).
range_each
мощна. Она длиной почти сто строк и имеет несколько версий. Я выделю несколько, объединив их вместе:
static VALUE range_each(VALUE range) { //... range_each_fixnum_endless(beg); range_each_fixnum_loop(beg, end, range); range_each_bignum_endless(beg); rb_str_upto_endless_each(beg, sym_each_i, 0); // и другие...
Эти функции C обрабатывают все вариации интервалов, которые вы можете использовать в своём коде:
(0...).each (0...100).each ("a"..."z").each # и так далее...
Почему важно, что Range#each
написана на C? Это означает, что YJIT не может исследовать её — оптимизации останавливаются на вызове функции и возобновляются после возвратов вызовов функции. Функции C быстры, но YJIT может ещё больше ускорять работу, создавая специализации для «горячих» путей исполнения кода. У Аарона Паттерсона есть отличная статья Ruby Outperforms C, из которой можно узнать о некоторых таких специализированных оптимизациях.
2. Оптимизируем цикл: Integer#times была переписана с C на Ruby в Ruby 3.3
«Горячий» путь (на котором тратится основное время CPU) — это Range#each
, то есть функция C. YJIT не может оптимизировать функции C, для него они остаются «чёрными ящиками». Так что же можно сделать?
Мы переписали Integer#times на Ruby в 3.3
Интересно! В Ruby 3.3, Integer#times
была переписана из функции C в метод Ruby! Вот версия из 3.3+, она довольно простая:
def times #... небольшой код для взаимодействия с C i = 0 while i < self yield i i = i.succ end self end
Очень просто. Это обычный цикл while. Но важнее всего то, что это код на Ruby, то есть YJIT сможет выполнить интроспекцию и оптимизировать его!
Примечание об Integer#succ
Немного странная часть этого кода — i.succ
. Я никогда не слышал о Integer#succ
, который, очевидно, возвращает «последующий элемент» integer.
В начале 2024 года был PR для повышения производительности Integer#succ
, помогший мне понять, зачем его вообще использовать:
Мы используем Integer#succ при переписывании методов циклов на Ruby (например, Integer#times и Array#each), потому что opt_succ (i = i.succ) быстрее диспетчеризировать для интерпретатора, чем putobject 1; opt_plus (i += 1).
Integer#succ
похож на чит-код виртуальной машины. Он получает стандартную операцию (прибавление 1 к integer) и превращает её из двух операций виртуальной машины в одну. Мы можем вызвать disasm
для метода times
, чтобы посмотреть на это в действии:
puts RubyVM::InstructionSequence.disasm(1.method(:times))
Метод Integer#times
разбивается на большой объём байт-кода Ruby VM, но нас волнуют только несколько строк:
... 0025 getlocal_WC_0 i@0 0027 opt_succ <calldata!mid:succ, ARGS_SIMPLE>[CcCr] 0029 setlocal_WC_0 i@0 ...
-
getlocal_WC_0
получает переменнуюi
из текущей области видимости. Этоi
вi.succ
-
opt_succ
выполняет вызовsucc
вi.succ
. Он выполняет вызов или самого методаInteger#succ
, или оптимизированной функции C для малых чисел -
В Ruby 3.4 со включенным YJIT малые числа ещё сильнее оптимизируются в машинный код (это просто примечание, этого не показано в машинном коде VM)
-
setlocal_WC_0
присваивает результатopt_succ
нашей локальной переменнойi
Если мы поменяем i = i.succ
на i += 1
, то вместо opt_succ
будут выполняться две операции VM :
... 0025 getlocal_WC_0 i@0 0027 putobject_INT2FIX_1_ 0028 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE> 0029 setlocal_WC_0 i@0 ...
По сути, всё осталось таким же, как раньше, но теперь нам нужно выполнять два этапа вместо одного:
-
putobject_INT2FIX_1_
записывает целое число1
в стек виртуальной машины -
opt_plus
— это+
в+= 1
, он вызывает или метод Ruby+
, или оптимизированную функцию C для малых чисел -
Вероятно, для
opt_plus
тоже существует YJIT-оптимизация
Главный вывод из этого кода можно сделать такой: на уровне VM и JIT выполняются глубокие оптимизации. При написании обычных программ на Ruby мы не учитываем и не должны учитывать разницу между одной или двумя командами машинного кода. Но на уровне JIT, в масштабе миллионов и миллиардов операций это важно!
Вернёмся к Integer#times
Давайте попробуем снова выполнить код бенчмарка с использованием times
! Вместо итераций по интервалам мы просто выполним итерации для 10_000
и 100_000
times
:
u = ARGV[0].to_i r = rand(10_000) a = Array.new(10_000, 0) 10_000.times do |i| 100_000.times do |j| a[i] += j % u end a[i] += r end puts a[r]
Loops |
|
---|---|
Range#each |
25,57 с |
Integer#times |
13,66 с |
Отлично! При использовании Integer#times
влияние YJIT гораздо сильнее. Он существенно увеличивает скорость, снижая время на моей машине до 13,66 секунды. На машине @k0kobun время снизилось до 9 секунд (и до 8 секунд на Ruby 3.4).
Вероятно, чтобы время стало меньше 8 секунд, понадобится уже Ruby 3.5.
Стоит ожидать от Ruby 3.5 ещё более высокой производительности. Поглядим!
3. Array#each была переписана с C на Ruby в Ruby 3.4
В CRuby продолжается переписывание кода C на Ruby, и в Ruby 3.4 одним из таких изменений стал Array#each
. Вот пример первой попытки его реализации:
def each unless block_given? return to_enum(:each) { self.length } end i = 0 while i < self.length yield self[i] i = i.succ end self end
Очень просто и читаемо! И код можно оптимизировать YJIT!
К сожалению, из-за чего-то, связанного с внутренним устройством CRuby, он содержал условия гонки. В Ruby 3.4 была внедрена более поздняя реализация.
def each Primitive.attr! :inline_block, :c_trace unless defined?(yield) return Primitive.cexpr! 'SIZED_ENUMERATOR(self, 0, 0, ary_enum_length)' end _i = 0 value = nil while Primitive.cexpr!(%q{ ary_fetch_next(self, LOCAL_PTR(_i), LOCAL_PTR(value)) }) yield value end self end
В отличие от первой реализации и от Integer#times
, здесь всё стало немного загадочнее. Это определённо не тот чистый код на Ruby, который следует писать. Почему-то модуль Primitive
как будто позволяет выполнять код на C из Ruby, и благодаря этому он избегает условий гонки, присутствующих в решении на чистом Ruby3.
Я думаю, что благодаря получению индексов и значений при помощи кода на C операция становится более атомарной. Понятия не имею, почему для возврата перечислителя используется Primitive.cexpr!
, а также какое значение передаёт Primitive.attr! :inline_block
.
На самом деле упомянутый выше исходный код Integer#times
тоже содержал синтаксис Primitive
. Ядро метода соответствует тому, что мы уже рассмотрели, оно полностью написано на Ruby, но в начале этого метода есть вызовы Primitive
для :inline_block
и возврат перечислителя:
def times Primitive.attr! :inline_block unless defined?(yield) return Primitive.cexpr! 'SIZED_ENUMERATOR(self, 0, 0, int_dotimes_size)' end #...
Да, это более загадочно, чем Integer#times
, но Array#each
по большей части написан на Ruby (в Ruby 3.4+). Давайте попробуем использовать массивы вместо интервалов и times
:
u = ARGV[0].to_i r = rand(10_000) a = Array.new(10_000, 0) outer = (0...10_000).to_a.freeze inner = (0...100_000).to_a.freeze outer.each do |i| inner.each do |j| a[i] += j % u end a[i] += r end puts a[r]
Несмотря на встроенный код на C, YJIT, похоже, по-прежнему способен выполнять приличные оптимизации производительности. Скорость оказывается примерно наравне с Integer#times
!
Loops |
|
---|---|
Range#each |
25,57 с |
Integer#times |
13,66 с |
Array#each |
13,96 с |
Микробенчмаркинг производительности Ruby
Я форкнул репозиторий сравнения языков и создал собственный репозиторий Ruby Microbench. Он использует все описанные выше примеры, а также множество других способов выполнения итераций в Ruby: https://github.com/jpcamara/ruby_microbench
Вот результаты его прогона в Ruby 3.4 с YJIT и без него:
fibonacci |
array#each |
range#each |
times |
for |
while |
loop do |
|
---|---|---|---|---|---|---|---|
Ruby 3.4 YJIT |
2,19 с |
14,02 с |
26,61 с |
13,12 с |
14,91 с |
37,10 с |
13,95 с |
Ruby 3.4 |
16,49 с |
34,29 с |
33,88 с |
33,18 с |
36,32 с |
37,14 с |
50,65 с |
Понятия не имею, почему написанный мной пример цикла while
такой медленный. Я ожидал, что он будет работать намного быстрее. Возможно, я написал его как-то не так — можете открыть issue или PR, если знаете, в чём ошибка. loop do
(взятый из примера @timtilberg) выполняется примерно с той же скоростью, что и Integer#times
, однако с отключенным YJIT его производительность ужасна.
Кроме запуска Ruby 3.4 я ради интереса воспользовался rbenv
для запуска:
-
Ruby 3.3
-
Ruby 3.3 YJIT
-
Ruby 3.2
-
Ruby 3.2 YJIT
-
TruffleRuby 24.1
-
Ruby Artichoke
-
MRuby
Ниже представлены некоторые из прогонов тестов:
fibonacci |
array#each |
range#each |
times |
for |
while |
loop do |
|
---|---|---|---|---|---|---|---|
Ruby 3.4 YJIT |
2,19 с |
14,02 с |
26,61 с |
13,12 с |
14,91 с |
37,10 с |
13,95 с |
Ruby 3.4 |
16,49 с |
34,29 с |
33,88 с |
33,18 с |
36,32 с |
37,14 с |
50,65 с |
TruffleRuby 24.1 |
0,92 с |
0,97 с |
0,92 с |
2,39 с |
2,06 с |
3,90 с |
0,77 с |
MRuby 3.3 |
28,83 с |
144,65 с |
126,40 с |
128,22 с |
133,58 с |
91,55 с |
144,93 с |
Artichoke |
19,71 с |
236,10 с |
214,55 с |
214,51 с |
215,95 с |
174,70 с |
264,67 с |
На основании этих данных я взял первоначальную визуализацию и создал визуализацию конкретно для Ruby и прогона fibonacci
:
Ускоряем range#each
Можем ли мы отдельно от @k0kobun сделать быстрее работу range#each
? Если я тупо заменю класс Range
реализацией на чистом Ruby, то всё действительно становится намного быстрее! Вот моя реализация:
class Range def each beginning = self.begin ending = self.end i = beginning loop do break if i == ending yield i i = i.succ end end end
А вот изменение в производительности — на 2 секунды медленнее, чем times
. Совсем неплохо!
Потраченное время |
|
---|---|
Range#each на C |
25,57 с |
Range#each на Ruby |
16,64 с |
Очевидно, что всё это чрезмерно упрощено. Я не обрабатываю различные случаи Range
и могу упускать какие-то нюансы. Кроме того, большинство виденных мной переписанных на Ruby методов вызывает для некоторых операций класс Primitive
. Мне бы хотелось больше узнать о том, когда и зачем это нужно.
Но! Это показывает мощь переноса кода с C и использования оптимизаций YJIT. Он может улучшать производительность такими способами, которые было бы сложно или невозможно воссоздать в обычном коде на C.
Стандартная библиотека YJIT
В прошлом году Аарон Паттерсон написал статью Ruby Outperforms C, в которой он переписал C-расширение на Ruby для парсинга GraphQL. Благодаря оптимизациям YJIT код на Ruby превзошёл по производительности код на C.
Это заставило меня задуматься о том, что было бы любопытно увидеть появление некой «стандартной библиотеки YJIT», в которой базовая функциональность Ruby, написанная на C, заменялась бы реализациями на Ruby для тех, кто пользуется YJIT.
Оказалось, именно этим и занимается команда разработчиков ядра YJIT. Во многих случаях они полностью избавились от кода на C, но недавно создали блок with_yjit
. Код будет задействоваться только при включенном YJIT, в противном случае будет работать код на C. Например, вот так реализован Array#each
:
with_yjit do if Primitive.rb_builtin_basic_definition_p(:each) undef :each def each # :nodoc: # ... мы рассматривали этот код ранее... end end end
В Ruby 3.3 YJIT можно инициализировать ленивым образом. К счастью, этим занимается код with_yjit
— если включен YJIT, то будут выполняться соответствующие версии методов with_yjit
:
# Использует встроенный C [1, 2, 3].each do |i| puts i end RubyVM::YJIT.enable # Использует версию на Ruby, которую можно оптимизировать YJIT [1, 2, 3].each do |i| puts i end
Это связано с тем, что with_yjit
— это «хук» YJIT, вызываемый в момент включения YJIT. После вызова он удаляется из среды выполнения при помощи undef :with_yjit
.
Исследуем оптимизации YJIT
Мы видели код на Ruby и видели код на C. Мы видели байт-код Ruby VM. Почему бы не сделать ещё один шаг вперёд и не рассмотреть машинный код? А может даже код на Rust? Постойте, не торопитесь сбегать!
Если вы не сбежали и не перескочили к следующему разделу, давайте рассмотрим небольшой кусочек YJIT!
Можно изучить машинный код, генерируемый YJIT. Для этого нужно собрать CRuby из исходников с флагами отладки YJIT. Если вы работаете на Mac, то можете посмотреть мою настройку MacOS для хакинга с CRuby или мою настройку docker для хакинга с CRuby, там есть более подробные инструкции по сборке Ruby. Самое простое — это перейти к ./configure
Ruby и передать опцию --enable-yjit=dev
:
./configure --enable-yjit=dev make install
В качестве примера кода на Ruby давайте используем пример Integer#times
:
u = ARGV[0].to_i r = rand(10_000) a = Array.new(10_000, 0) 10_000.times do |i| 100_000.times do |j| a[i] += j % u end a[i] += r end puts a[r]
Так как мы собирали Ruby с YJIT в режиме разработки, при запуске программы ruby можно передать флаг --yjit-dump-disasm
:
./ruby --yjit --yjit-dump-disasm test.rb 40
Благодаря этому мы сможем просмотреть созданный машинный код. Сосредоточимся только на одной маленькой части — эквиваленте в машинном коде байт-кода Ruby VM, который мы читали ранее. Вот первоначальный байт-код VM для opt_succ
, сгенерированный при вызове i.succ
метода Integer#succ
:
... 0027 opt_succ <calldata!mid:succ, ARGS_SIMPLE>[CcCr] ...
А вот машинный код, который генерирует в этом сценарии YJIT на моём Mac M2 с архитектурой arm64:
# Block: times@<internal:numeric>:259 # reg_mapping: [Some(Stack(0)), None, None, None, None] # Insn: 0027 opt_succ (stack_size: 1) # call to Integer#succ # guard object is fixnum 0x1096808c4: tst x1, #1 0x1096808c8: b.eq #0x109683014 0x1096808cc: nop 0x1096808d0: nop 0x1096808d4: nop 0x1096808d8: nop 0x1096808dc: nop # Integer#succ 0x1096808e0: adds x11, x1, #2 0x1096808e4: b.vs #0x109683048 0x1096808e8: mov x1, x11
Честно говоря, я понимаю 25% из этого, а для изучения остальных 75% использовал ИИ и собственную логику. Можете спокойно критиковать меня, если я в чём-то немного ошибся, я люблю узнавать новое. Разберём всё по порядку.
# Block: times@<internal:numeric>:259
Это приблизительно соответствует строке i = i.succ
в методе Integer#times
в numeric.rb
. Я говорю «приблизительно», потому что в своём текущем коде я вижу это в строке 258, но, возможно, он показывает конец блока, который он выполнял, поскольку YJIT компилирует «блоки» кода:
256: while i < self 257: yield i 258: i = i.succ 259: end # reg_mapping: [Some(Stack(0)), None, None, None, None] # Insn: 0027 opt_succ (stack_size: 1) # call to Integer#succ
Понятия не имею, что значит reg_mapping
— возможно, отображение того, как он использует регистр CPU? Insn: 0027 opt_succ
выглядит очень знакомо! Это наш байт-код VM! call to Integer#succ
— это просто полезный комментарий. YJIT способен добавлять комментарии в машинный код. Мы всё ещё из них не выбрались.
# guard object is fixnum
А это любопытно. Я смог найти соответствующий фрагмент кода на Rust, отображаемый непосредственно в это. Давайте рассмотрим его:
fn jit_rb_int_succ( //... asm: &mut Assembler, //... ) -> bool { // Guard the receiver is fixnum let recv_type = asm.ctx.get_opnd_type(StackOpnd(0)); let recv = asm.stack_pop(1); if recv_type != Type::Fixnum { asm_comment!(asm, "guard object is fixnum"); asm.test(recv, Opnd::Imm(RUBY_FIXNUM_FLAG as i64)); asm.jz(Target::side_exit(Counter::opt_succ_not_fixnum)); } asm_comment!(asm, "Integer#succ"); let out_val = asm.add(recv, Opnd::Imm(2)); // 2 is untagged Fixnum 1 asm.jo(Target::side_exit(Counter::opt_succ_overflow)); // Push the output onto the stack let dst = asm.stack_push(Type::Fixnum); asm.mov(dst, out_val); true }
Отлично! Это настоящая реализация YJIT Rust вызова opt_succ
. Именно эту оптимизацию внёс @k0kubun, чтобы ещё больше повысить производительность opt_succ
по сравнению с вызовами функций на C. Мы находимся в части, проверяющей, работаем ли мы с Fixnum — это способ хранения малых integer внутри CRuby:
if recv_type != TypeFixnum asm_comment!(asm, "guard object is fixnum"); asm.test(recv, Opnd::Imm(RUBY_FIXNUM_FLAG as i64)); asm.jz(Target::side_exit(Counter::opt_succ_not_fixnum)); }
Это превращается в такой машинный код:
# guard object is fixnum 0x1096808c4: tst x1, #1 0x1096808c8: b.eq #0x109683014
asm.test
генерирует tst x1, #1
, что по мнению ИИ-бота, которому я задал вопрос, проверяет младший бит, являющийся «меткой» Fixnum, обозначающей, что это Fixnum. Если это Fixnum, то результат равен 1, а b.eq
равен false. Если это не Fixnum, результат равен 0
, а b.eq
равен true и выполняется переход из этого кода.
0x1096808cc: nop 0x1096808d0: nop 0x1096808d4: nop 0x1096808d8: nop 0x1096808dc: nop
«NOP для выравнивания/заполнения». Спасибо, ИИ! Я не знаю, зачем это нужно, но, по крайней мере, знаю, чем это может быть.
Наконец мы действительно прибавляем 1 к числу.
asm_comment!(asm, "Integer#succ"); let out_val = asm.add(recv, Opnd::Imm(2)); // 2 is untagged Fixnum 1 asm.jo(Target::side_exit(Counter::opt_succ_overflow)); // Push the output onto the stack let dst = asm.stack_push(Type::Fixnum); asm.mov(dst, out_val);
Код на Rust генерирует наш комментарий Integer#succ
. Для прибавления 1, поскольку данные «метки Fixnum» встроены в integer, на самом деле нужно прибавить 2 при помощи adds x11, x1, #2
. Если произойдёт переполнение доступного пространства, то выполнится выход по другому пути кода: b.vs
— это ветвление при переполнении. В противном случае результат сохраняется при помощи mov x1, x11
!
# Integer#succ 0x1096808e0: adds x11, x1, #2 0x1096808e4: b.vs #0x109683048 0x1096808e8: mov x1, x11
Да уж, большой объём информации. И, похоже, здесь выполняется много работы, но поскольку это такой низкоуровневый машинный код, он, предположительно, очень быстрый. Мы изучили лишь самую крошечную часть того, что способен генерировать YJIT; JIT очень сложные!
Благодарю @k0kubun за помощь и ссылку на документацию YJIT, в которой тоже содержится множество дополнительных опций.
Будущее оптимизаций CRuby
Ирония реализации языка заключается в том, что часто вы меньше работаете с языком, который реализуете, чем на более низком уровне: в случае Ruby это в основном C и немного Rust.
Благодаря слою наподобие YJIT в будущем потенциально бо́льшая часть языка станет чистым Ruby, и разработчикам на Ruby будет проще вносить свой вклад. У многих языков есть маленькое низкоуровневое ядро, а основная часть языка написана на нём самом (как, например, Java). Возможно, когда-нибудь это станет и будущим CRuby! А пока пусть команда разработчиков YJIT создаёт новые оптимизации YJIT!
-
Под «наивным» в данном случае подразумевается, что есть более эффективные способы программного вычисления чисел Фибоначчи
-
Предок YJIT под названием MJIT сделал Ruby гораздо быстрее в некоторых бенчмарках. Но в больших реальных приложениях Rails он на самом деле замедлял работу
-
При выполнении кода на C он сам выбирает, нужно ли отключать GVL, чтобы потокам было сложнее повреждать или модифицировать данные посередине операции. Первоначальная версия на Ruby могла получать GVL в точках, способных сделать массив невалидным. По крайней мере, я понимаю ситуацию так.
ссылка на оригинал статьи https://habr.com/ru/articles/864504/
Добавить комментарий