вторник, 25 февраля 2020 г.

Почему существуют тривиальные функции типа CopyRect и EqualRect?

Это перевод Why are there trivial functions like Copy­Rect and Equal­Rect? Автор: Реймонд Чен.

Если вы покопаетесь внутри user32, вы увидите там некоторые, казалось бы, тривиальные функции - такие как CopyRect и EqualRect. Зачем нам вообще нужны целые функции для того, что можно сделать с помощью операторов := и =?

Краткий ответ: потому что эти операторы генерируют кучу кода.

Длинный ответ: посмотрите на код, который генерируется при использовании оператора присвоения (копирование прямоугольника):
c4 5e f0        les  bx, [bp-10]    ; es:bx -> исходный прямоугольник (source)
26 8b 07        mov  ax, es:[bx]    ; ax = source.left
c4 5e ec        les  bx, [bp-14]    ; es:bx -> целевой прямоугольник (dest)
26 89 07        mov  es:[bx], ax    ; dest.left = ax

c4 5e f0        les  bx, [bp-10]    ; es:bx -> исходный прямоугольник (source)
26 8b 47 02     mov  ax, es:[bx+2]  ; ax = source.top
c4 5e ec        les  bx, [bp-14]    ; es:bx -> целевой прямоугольник (dest)
26 89 47 02     mov  es:[bx+2], ax  ; dest.top = ax

c4 5e f0        les  bx, [bp-10]    ; es:bx -> исходный прямоугольник (source)
26 8b 47 04     mov  ax, es:[bx+4]  ; ax = source.right
c4 5e ec        les  bx, [bp-14]    ; es:bx -> целевой прямоугольник (dest)
26 89 47 04     mov  es:[bx+4], ax  ; dest.right = ax

c4 5e f0        les  bx, [bp-10]    ; es:bx -> исходный прямоугольник (source)
26 8b 47 06     mov  ax, es:[bx+6]  ; ax = source.bottom
c4 5e ec        les  bx, [bp-14]    ; es:bx -> целевой прямоугольник (dest)
26 89 47 06     mov  es:[bx+6], ax  ; dest.bottom = ax
Этот код занимает 54 байт! Он получился ужасно неэффективным, потому что процессор 8086 может использовать непрямую адресацию только через регистры bx, bp, si и di. Регистр bp зарезервирован для использования в качестве указателя фрейма, так что его можно сразу вычеркнуть. Регистры si и di используются как регистры-переменные, поэтому в них почти всегда лежит что-то нужное. Что оставляет нам единственный регистр, который можно использовать для разыменования указателей: bx.

Поскольку мы работаем с указателем 16:16 - нам также нужен регистр сегмента, а у 8086 есть только четыре сегментных регистра: cs (сегмент кода), ds (сегмент данных), ss (сегмент стека), es (дополнительный сегмент). У трёх из них уж есть предназначенные цели, поэтому остаётся только один: es. Так что даже если бы мы могли временно использовать si или di, мы всё равно оказались бы в узком месте.

Но если мы переместим код в функцию CopyRect, то мы можем сэкономить кучу байт:
c4 5e f0        les  bx, [bp-10]    ; es:bx -> исходный прямоугольник (source)
53              push bx
06              push es
c4 5e ec        les  bx, [bp-14]    ; es:bx -> целевой прямоугольник (dest)
53              push bx
06              push es
9a xx xx xx xx  call CopyRect
Всего 15 байт! Экономия почти в ЧЕТЫРЕ раза.

Это была эпоха экономии байт, поэтому любой приём, позволяющий сэкономить несколько байтов, заслуживал рассмотрения - особенно если учесть, что у вас было "всего" 256 Кб памяти¹.
А поскольку копирование и сравнение прямоугольников были весьма частыми операциями, сворачивание кода в функцию позволяло сэкономить много байтов.

Конечно, сегодня требуется уже не так много кода, чтобы скопировать прямоугольник вручную: весь прямоугольник помещается в один 128-битный регистр:
    mov    eax, [sourcerect]
    movups xmm0, [eax]
    mov    eax, [destrect]
    movups [eax], xmm0

¹ Слово "всего" взято в кавычки, потому что 256 Кб кажется ужасно малым размером памяти сегодня, но вспомните, что в те времена это было максимумом, который вы могли поставить в IBM PC XT! По крайней мере, не прибегая к использованию карт расширений.


Бонусное обсуждение: мы могли бы убрать несколько инструкций, перемещая по два целых числа за раз. Это требует, чтобы два прямоугольника не перекрывались бы в памяти (чтобы избежать наложения данных) - но это, вероятно, безопасное предположение, потому что исходный код в этом случае так же не работал.
var
  R1, R2: PRect;
  V: array[0..5] of Byte;
begin
  R1 := @V[0];
  R2 := @V[1]; // плохая идея
Хорошо, раз переключение на перемещение двух целых чисел за раз не нарушает ничего, что ещё не было сломано, давайте сделаем это:
c4 5e f0        les  bx, [bp-10]    ; es:bx -> исходный прямоугольник (source)
26 8b 07        mov  ax, es:[bx]    ; ax = source.left
26 8b 57 02     mov  dx, es:[bx+2]  ; dx = source.top
c4 5e ec        les  bx, [bp-14]    ; es:bx -> целевой прямоугольник (dest)
26 89 07        mov  es:[bx], ax    ; dest.left = ax
26 89 57 02     mov  es:[bx+2], dx  ; dest.top = dx

c4 5e f0        les  bx, [bp-10]    ; es:bx -> исходный прямоугольник (source)
26 8b 47 04     mov  ax, es:[bx+4]  ; ax = source.right
26 8b 57 06     mov  dx, es:[bx+6]  ; dx = source.bottom
c4 5e ec        les  bx, [bp-14]    ; es:bx -> целевой прямоугольник (dest)
26 89 47 04     mov  es:[bx+4], ax  ; dest.right = ax
26 89 57 06     mov  es:[bx+6], dx  ; dest.bottom = dx
Получаем 42 байта (против бывших 54). Уже лучше, но это всё равно почти в три раза больше вызова функции.

Если у нас есть одна дополнительная свободная переменная (скажем, si), то мы можем сократить этот код ещё больше:
c4 5e f0        les  bx, [bp-10]    ; es:bx -> исходный прямоугольник (source)
26 8b 07        mov  ax, es:[bx]    ; ax = source.left
26 8b 57 02     mov  dx, es:[bx+2]  ; dx = source.top
26 8b 4f 04     mov  cx, es:[bx+4]  ; cx = source.right
26 8b 77 06     mov  si, es:[bx+6]  ; si = source.bottom
c4 5e ec        les  bx, [bp-14]    ; es:bx -> целевой прямоугольник (dest)
26 89 07        mov  es:[bx], ax    ; dest.left = ax
26 89 57 02     mov  es:[bx+2], dx  ; dest.top = dx
26 89 4f 04     mov  es:[bx+4], cx  ; dest.right = cx
26 89 77 06     mov  es:[bx+6], si  ; dest.bottom = si
Всего 36 байт. Уже лучше, но всё ещё более чем в два раза больше, чем вызов функции. И это также стоило нам одного регистра.

Вот ещё один трюк: копирование прямоугольника через стек.
c4 5e f0        les  bx, [bp-10]    ; es:bx -> исходный прямоугольник (source)
26 ff 37        push es:[bx]        ; push source.left
26 ff 77 02     push es:[bx+2]      ; push source.top
26 ff 77 04     push es:[bx+4]      ; push source.right
26 8b 77 06     push es:[bx+6]      ; push source.bottom
c4 5e ec        les  bx, [bp-14]    ; es:bx -> целевой прямоугольник (dest)
26 8f 47 06     pop  es:[bx+6]      ; pop dest.bottom
26 8f 47 04     pop  es:[bx+4]      ; pop dest.right
26 8f 47 02     pop  es:[bx+2]      ; pop dest.top
26 8f 47        pop  es:[bx]        ; pop dest.left
Хм, получается столько же, сколько и с регистрами. Но хотя бы в этот раз нам не потребовался свободный регистр.

Хорошо, а если мы позаимствуем регистр ds, а также и si и di?
1e              push ds
c5 7e ec        lds  di, [bp-14]
c4 76 f0        les  si, [bp-10]
fc              cld
a5              movsw
a5              movsw
a5              movsw
a5              movsw
1f              pop  ds
Ух ты! Тринадцать байт!

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.