Эффективность присваивания и сравнения в одном операторе в Smalltalk

предыдущий вопрос SO поднял вопрос о том, какая идиома лучше с точки зрения эффективности выполнения:

[ (var := exp) > 0 ] whileTrue: [ ... ]

против

[ var := exp. 
  var > 0 ] whileTrue: [ ... ]

Интуитивно кажется, что первая форма может быть более эффективной во время выполнения, потому что она экономит выборку одного дополнительного оператора (вторая форма). Верно ли это для большинства Smalltalks?

Попытка с двумя глупыми тестами:

| var acc |
var := 10000.
[ [ (var := var / 2) < 0  ] whileTrue: [ acc := acc + 1 ] ] bench.

| var acc |
var := 10000.
[ [ var := var / 2. var < 0  ] whileTrue: [ acc := acc + 1 ] ] bench

Не выявляет серьезных различий между обеими версиями.

Любые другие мнения?


person user1000565    schedule 07.07.2018    source источник
comment
Вы повторяете это достаточно раз, чтобы сделать измеримый эталон? Я не знаю smalltalk (только бенчмаркинг и язык ассемблера), но похоже, что ваш цикл выполняется только log2 (10000) раз, что очень и очень коротко. (Или это 1 раз, потому что 10000/2 < 0 ложно?). Первый тест заключается в том, масштабируется ли время линейно с количеством повторений, если у вас есть это внутри цикла, который выполняет это многократно. Во-вторых, масштабируется ли вообще время с помощью var. IDK, как компилируется и интерпретируется реализация, но с постоянным аргументом времени компиляции он может просто оптимизировать до acc += constant.   -  person Peter Cordes    schedule 07.07.2018
comment
Как и во многих других языках, я думаю, что эти два случая, вероятно, будут компилироваться/интерпретироваться в одно и то же, поскольку компилятор или интерпретатор будет достаточно умен, чтобы понять, что вы используете результат присваивания сразу после присваивания.   -  person lurker    schedule 08.07.2018
comment
пожалуйста, не забудьте отметить ваши вопросы отвеченными, когда вы довольны ответом! Подробнее см. stackoverflow.com/help/someone-answers.   -  person tukan    schedule 09.07.2018


Ответы (1)


Итак, возникает вопрос: Что мне следует использовать, чтобы добиться лучшего времени выполнения?

temp := <expression>.
temp > 0

or

(temp := <expression>) > 0

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

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

Поскольку <expression> одинакова в обоих случаях, давайте резко уменьшим ее, чтобы устранить шум. Кроме того, давайте поместим наш код в метод, чтобы иметь CompiledMethod для игры.

Object >> m
  | temp |
  temp := 1.
  temp > 0

Теперь давайте посмотрим на CompiledMethod и его суперклассы на наличие сообщения, которое показало бы нам байт-коды Object >> #m. Селектор должен содержать байт-коды подслов, верно?

...

Вот оно #symbolicBytecodes! Теперь давайте оценим (Object >> #m) symbolicBytecodes, чтобы получить:

pushConstant: 1
popIntoTemp: 0
pushTemp: 0
pushConstant: 0
send: >
pop
returnSelf

Кстати, обратите внимание, как наша переменная temp была переименована в Temp: 0 на языке байт-кодов.

Теперь повторите с другим и получите:

pushConstant: 1
storeIntoTemp: 0
pushConstant: 0
send: >
pop
returnSelf

Разница в том,

popIntoTemp: 0
pushTemp: 0

против

storeIntoTemp: 0

Это показывает, что в обоих случаях temp читается из стека по-разному. В первом случае результат нашего <expression> извлекается из стека выполнения в temp, а затем снова помещается temp для восстановления стека. За pop следует push того же самого. Вместо этого во втором случае push или pop не происходит, а temp просто считывается из стека.

Таким образом, вывод таков, что в первом случае мы будем генерировать две инструкции отмены pop, за которыми следует push.

Это также объясняет, почему разницу так трудно измерить: инструкции push и pop имеют прямой перевод в машинный код, и ЦП будет выполнять их очень быстро.

Обратите внимание, однако, что ничто не мешает компилятору автоматически оптимизировать код и понять, что на самом деле pop + push эквивалентно storeInto. С такой оптимизацией оба фрагмента Smalltalk приведут к одному и тому же машинному коду.

Теперь вы должны решить, какую форму вы предпочитаете. На мой взгляд, такое решение должно учитывать только тот стиль программирования, который вам больше нравится. Принятие во внимание времени выполнения не имеет значения, потому что разница минимальна и может быть легко сведена к нулю путем реализации оптимизации, которую мы только что обсуждали. Между прочим, это было бы отличным упражнением для тех, кто хочет понять низкоуровневые области беспрецедентного языка Smalltalk.

person Leandro Caniglia    schedule 07.07.2018
comment
В VisualWorks оба ваших примера кода компилируются в один и тот же байт-код, поэтому предложение Леандро по автоматической оптимизации кода фактически уже реализовано в VisualWorks. В результате декомпиляция обоих операторов приводит к (var := exp) › 0. Лично я предпочитаю разделять операторы, так как их гораздо легче понять. Не имеет прямого отношения к этому вопросу бенчмаркинга, но общий совет: старайтесь избегать циклов while/for в Smalltalk, они слишком сложны для понимания в целом по сравнению с типичными методами итерации, такими как #do:, #detect:, #select : и #собирать:. - person Karsten; 07.07.2018
comment
@Karsten большое спасибо за проверку в VisualWorks. - person Leandro Caniglia; 07.07.2018