Оптимизация байт-кода Smalltalk стоит усилий?

Рассмотрим следующий метод в классе Juicer:

Juicer >> juiceOf: aString
    | fruit juice |
    fruit := self gather: aString.
    juice := self extractJuiceFrom: fruit.
    ^juice withoutSeeds

Он генерирует следующие байт-коды

25 self                     ; 1
26 pushTemp: 0              ; 2
27 send: gather:
28 popIntoTemp: 1           ; 3
29 self                     ; 4
30 pushTemp: 1              ; 5
31 send: extractJuiceFrom:
32 popIntoTemp: 2           ; 6 <-
33 pushTemp: 2              ; 7 <-
34 send: withoutSeeds
35 returnTop

Теперь обратите внимание, что 32 и 33 отменят:

25 self                     ; 1
26 pushTemp: 0              ; 2
27 send: gather:
28 popIntoTemp: 1           ; 3 *
29 self                     ; 4 *
30 pushTemp: 1              ; 5 *
31 send: extractJuiceFrom:
32 storeIntoTemp: 2         ; 6 <-
33 send: withoutSeeds
34 returnTop

Далее рассмотрим 28, 29 и 30. Они вставляют self ниже результата gather. Такая же конфигурация стека могла быть достигнута путем нажатия self перед отправкой первого сообщения:

25 self                     ; 1 <-
26 self                     ; 2
27 pushTemp: 0              ; 3
28 send: gather:
29 popIntoTemp: 1           ; 4 <-
30 pushTemp: 1              ; 5 <-
31 send: extractJuiceFrom:
32 storeIntoTemp: 2         ; 6
33 send: withoutSeeds
34 returnTop

Теперь отмените 29 и 30

25 self                     ; 1
26 self                     ; 2
27 pushTemp: 0              ; 3
28 send: gather:
29 storeIntoTemp: 1         ; 4 <-
30 send: extractJuiceFrom:
31 storeIntoTemp: 2         ; 5
32 send: withoutSeeds
33 returnTop

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

25 self                     ; 1
26 self                     ; 2
27 pushTemp: 0              ; 3
28 send: gather:
29 send: extractJuiceFrom:
30 send: withoutSeeds
31 returnTop

Эта последняя версия, которая сохраняет 4 из 7 операций стека, соответствует менее выразительному и понятному источнику:

Juicer >> juiceOf: aString
    ^(self extractJuiceFrom: (self gather: aString)) withoutSeeds

Обратите также внимание на то, что существуют другие возможные оптимизации, которые Pharo (я не проверил Squeak) не реализует (например, цепочку переходов). Эти оптимизации будут побуждать программиста Smalltalk лучше выражать свои намерения, не оплачивая стоимость дополнительные вычисления.

Мой вопрос заключается в том, являются ли эти улучшения иллюзией или нет. Конкретно, оптимизация байт-кода отсутствует у Pharo/Squeak, поскольку они, как известно, мало релевантны или считаются полезными, но еще не были адресованы?

ИЗМЕНИТЬ

Интересное преимущество использования архитектуры register + stack [ср. Архитектурная модель виртуальной машины Smalltalk от Allen Wirfs-Brock и Pat Caudill] заключается в том, что дополнительное пространство, предоставляемое регистрами, облегчает манипулирование байт-кодами для ради оптимизации. Конечно, хотя эти виды оптимизации не так важны, как метод inline или полиморфные встроенные кэши, как указано в ответе ниже, их не следует игнорировать, особенно в сочетании с другими, реализованными JIT-компилятором. Еще одна интересная тема для анализа заключается в том, действительно ли необходима деструктивная оптимизация (т.е. Та, которая требует де-оптимизации для поддержки отладчика), или достаточное повышение производительности может быть достигнуто с помощью неразрушающих методов.

Ответ 1

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

Исторически и все еще в настоящее время в Squeak, отладчик моделирует уровень байт-кода и ему необходимо сопоставить байт-коды с соответствующей инструкцией Smalltalk.

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

Pharo хочет изменить отладчик для работы на более высоком уровне (абстрактное синтаксическое дерево), но я не знаю, как они закончится с байт-кодом, о котором все знают VM.

IMO, такая оптимизация может быть лучше реализована в компиляторе JIT, который преобразует байт-код в машинный код.

ИЗМЕНИТЬ

Наибольший выигрыш заключается в устранении самих отправок (путем inlining), потому что они намного дороже (x10), чем операции с стеком - в секунду выполняется в 10 раз больше байт-кодов, чем отправляется, когда вы тестируете 1 tinyBenchmarks (COG VM).

Интересно, что такая оптимизация может иметь место на изображении Smalltalk, но только на горячей точке, обнаруженной VM, как в попытке SISTA. См. Например https://clementbera.wordpress.com/2014/01/22/the-sista-chronicles-iii-an-intermediate-representation-for-optimizations/

Итак, в свете SISTA ответ скорее: интересный, еще не рассмотрен, но активно изучается (и работает в процессе)!

Все механизмы для де-оптимизации, когда метод нужно отлаживать, все еще являются одним из трудных моментов, насколько я понимаю.

Ответ 2

Я думаю, что стоит ответить на более широкий вопрос: стоит ли байт-код? Bytecodes считались компактным и портативным представлением кода, который близок к целевой машине. Таким образом, их легко интерпретировать, но медленно выполнять.

Bytecodes не превосходят ни в одной из этих игр, и это обычно делает их не лучшим выбором, если вы хотите либо написать интерпретатор, либо быструю виртуальную машину. С одной стороны, узлы AST намного проще интерпретировать (только несколько типов node против множества разных байткодов). С другой стороны, с появлением компиляторов JIT стало ясно, что запуск собственного кода не только возможен, но и намного быстрее.

Если вы посмотрите на наиболее эффективные версии Java для JavaScript (которые можно считать самыми современными компиляторами сегодня), а также Java (HotSpot, Grail), вы увидите, что все они используют многоуровневую схему компиляции. Методы изначально интерпретируются из АСТ и только дрожат, когда они становятся горячей точкой.

На самых сложных уровнях компиляции нет байт-кодов. Ключевым компонентом в компиляторе является его промежуточное представление, а байт-коды не выполняют требуемые свойства. Наиболее оптимизируемые ИК-массивы гораздо более мелкие: они находятся в форме SSA и позволяют конкретное представление регистров и памяти. Это позволяет значительно улучшить анализ и оптимизацию кода.

И снова, если вы заинтересованы в переносном коде, нет ничего более портативного, чем AST. Кроме того, проще и практичее применять отладчики и профилирующие устройства на основе AST, чем байт-коды. Единственная оставшаяся проблема - компактность, но в любом случае вы можете реализовать что-то вроде ast-code (закодированные asts, похожие на байт-коды, но представляющие дерево)

С другой стороны, если вы хотите получить полную скорость, вы пойдете на JIT с хорошим IR и без байт-кодов. Я думаю, что байт-коды не заполняют многие пробелы в сегодняшних виртуальных машинах, но по-прежнему остаются в основном для обратной совместимости (также есть много примеров аппаратных архиваторов, которые непосредственно выполняют байт-коды Java).

Есть также несколько интересных экспериментов с Cog VM, связанным с байт-кодами. Но из того, что я понимаю, они преобразуют байт-код в другой IR для оптимизации, а затем конвертируют обратно в байт-коды. Я не уверен, есть ли технический выигрыш в последнем преобразовании, кроме повторного использования исходной архитектуры JIT, или если на уровне байт-кода действительно есть какая-либо оптимизация.