Почему в C/C++/rtl нет функциональности Z80, подобной LDIR?

В машинном коде Z80 это дешевый метод инициализации буфера фиксированным значением, скажем, всеми пробелами. Таким образом, кусок кода может выглядеть примерно так.

LD HL, DESTINATION             ; point to the source
LD DE, DESTINATION + 1         ; point to the destination
LD BC, DESTINATION_SIZE - 1    ; copying this many bytes
LD (HL), 0X20                  ; put a seed space in the first position
LDIR                           ; move 1 to 2, 2 to 3...

В результате кусок памяти в DESTINATION полностью заполнен пустым. Я экспериментировал с memmove и memcpy и не могу воспроизвести это поведение. Я ожидал, что memmove сможет сделать это правильно.

Почему memmove и memcpy ведут себя таким образом?

Есть ли разумный способ выполнить такую ​​инициализацию массива?

Я уже знаю о char array[size] = {0} для инициализации массива

Я уже знаю, что memset будет работать с отдельными символами.

Какие еще существуют подходы к этому вопросу?


person EvilTeach    schedule 22.12.2008    source источник


Ответы (14)


Я считаю, что это соответствует философии дизайна C и C++. Как Бьярн Страуструп один раз сказал, один из основных руководящих принципов разработки C++: "Что вы не используете, вы не платите за то, за что вы не платите". И хотя Деннис Ричи, возможно, сказал это не совсем теми же словами, я считаю, что принцип, лежащий в основе его дизайна C (и дизайна C последующими людьми). Теперь вы можете подумать, что если вы выделяете память, она должна автоматически инициализироваться значением NULL, и я склонен с вами согласиться. Но это требует машинных циклов, и если вы кодируете в ситуации, когда каждый цикл имеет решающее значение, это может быть неприемлемым компромиссом. В основном C и C++ стараются держаться подальше от вас, поэтому, если вы хотите что-то инициализировать, вы должны сделать это самостоятельно.

person Onorio Catenacci    schedule 22.12.2008

memmove и memcpy не работают таким образом, потому что это бесполезная семантика для перемещения или копирования памяти. В Z80 удобно иметь возможность заполнять память, но почему вы ожидаете, что функция с именем «memmove» заполнит память одним байтом? Это для перемещения блоков памяти. Это реализовано для получения правильного ответа (байты источника перемещаются в место назначения) независимо от того, как перекрываются блоки. Это полезно, чтобы получить правильный ответ для перемещения блоков памяти.

Если вы хотите заполнить память, используйте memset, который предназначен именно для того, что вам нужно.

person Ned Batchelder    schedule 22.12.2008

Был более быстрый способ очистить область памяти с помощью стека. Хотя использование LDI и LDIR было очень распространенным, Дэвид Уэбб (который продвигал ZX Spectrum всевозможными способами, такими как обратный отсчет чисел в полноэкранном режиме, включая границу) придумал эту технику, которая работает в 4 раза быстрее:

  • сохраняет указатель стека, а затем перемещает его в конец экрана.
  • ЗАГРУЖАЕТ пару регистров HL нулем,
  • переходит в массивный цикл, ПУШЯ HL в стек.
  • Стек перемещается вверх по экрану и вниз по памяти, очищая при этом экран.

Объяснение выше взято из обзора игры Дэвида Уэбба Starion.

Процедура Z80 может выглядеть примерно так:

  DI              ; disable interrupts which would write to the stack.
  LD HL, 0
  ADD HL, SP      ; save stack pointer
  EX DE, HL       ; in DE register
  LD HL, 0
  LD C, 0x18      ; Screen size in pages
  LD SP, 0x4000   ; End of screen
PAGE_LOOP:
  LD B, 128       ; inner loop iterates 128 times
LOOP:
  PUSH HL         ; effectively *--SP = 0; *--SP = 0;
  DJNZ LOOP       ; loop for 256 bytes
  DEC C
  JP NZ,PAGE_LOOP
  EX DE, HL
  LD SP, HL       ; restore stack pointer
  EI              ; re-enable interrupts

Тем не менее, эта процедура чуть менее чем в два раза быстрее. LDIR копирует один байт каждые 21 цикл. Внутренний цикл копирует два байта каждые 24 цикла — 11 циклов для PUSH HL и 13 для DJNZ LOOP. Чтобы получить почти в 4 раза больше скорости, просто разверните внутренний цикл:

LOOP:
   PUSH HL
   PUSH HL
   ...
   PUSH HL         ; repeat 128 times
   DEC C
   JP NZ,LOOP

Это почти 11 циклов на каждые два байта, что примерно в 3,8 раза быстрее, чем 21 цикл на байт LDIR.

Несомненно, техника неоднократно изобреталась заново. Например, ранее он появлялся в Симулятор полета 1 компании sub-Logic для TRS-80 в 1980 году.

person devstopfix    schedule 23.12.2008
comment
Прошло несколько лет с тех пор, как я что-то делал с Z80, но мне это кажется хорошим. Я бы, конечно, добавил в конце «LD SP, DE». - person David Thornley; 24.12.2008
comment
Еще более быстрый способ, который я использовал, — поместить в цикл несколько инструкций PUSH HL. Таким образом, если вы очищаете, скажем, 2 КБ памяти, вы можете использовать 16 PUSH HL и зацикливаться только на 2 КБ/16 (256) раз. - person Mike Thompson; 09.01.2009
comment
DEC не устанавливает нулевой флаг. На самом деле он не устанавливает никакого флага. - person Sedat Kapanoglu; 02.03.2009
comment
16-битный DEC не устанавливает никаких флагов, а 8-битный DEC устанавливает. Переписывание цикла во внутренний цикл по C и внешний цикл по B позаботится об этой проблеме, как и использование DJNZ, который является IIRC быстрее, чем DEC B; JNZ LOOP отдельно. Конечно, для этого потребуется, чтобы внутренний цикл находился над B... - person RBerteig; 15.04.2009
comment
Как сказал @RBerteig, 16-битные инструкции DEC не влияют на флаги, поэтому необходимо использовать 8-битный счетчик (или вложенный цикл с 8-битными счетчиками, если необходимо). Кроме того, я бы добавил DI в начале и EI в конце, чтобы не прерывать прерывания. - person Anders Marzi Tornblad; 01.07.2016

Почему так себя ведут memmove и memcpy?

Возможно, потому, что не существует специального современного компилятора C++, ориентированного на аппаратное обеспечение Z80? Напишите один. ;-)

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

Есть ли разумный способ выполнить такую ​​инициализацию массива? Есть ли разумный способ выполнить такую ​​инициализацию массива?

Что ж, если ничего не помогает, вы всегда можете использовать встроенную сборку. Кроме этого, я ожидаю, что std::fill будет работать лучше всего в хорошей реализации STL. И да, я полностью осознаю, что мои ожидания слишком высоки и что std::memset часто работает лучше на практике.

person Konrad Rudolph    schedule 22.12.2008
comment
Я не ищу компилятор z80. Я ищу метод, похожий на ldir, для инициализации буферов. - person EvilTeach; 23.12.2008
comment
Я не ответил, так как не понимаю вопроса. Инициализировать буферы в C++ можно с помощью std::fill (или memset, или wmemset, или непереносимых эквивалентов для больших значений). Почему тебе это не нравится? Какова мотивация требования, похожего на LDIR, вам просто нравится эта идиома? - person Steve Jessop; 23.12.2008

Показанная вами последовательность Z80 была самым быстрым способом сделать это в 1978 году. Это было 30 лет назад. Процессоры сильно продвинулись с тех пор, и сегодня это едва ли не самый медленный способ сделать это.

Memmove предназначен для работы, когда диапазоны источника и назначения перекрываются, поэтому вы можете переместить фрагмент памяти на один байт вверх. Это часть его поведения, определенного стандартами C и C++. Memcpy не указан; он может работать идентично memmove или отличаться, в зависимости от того, как ваш компилятор решит его реализовать. Компилятор может выбрать более эффективный метод, чем memmove.

person Mark Ransom    schedule 23.12.2008

Если вы возитесь на аппаратном уровне, то некоторые ЦП имеют контроллеры DMA, которые могут чрезвычайно быстро заполнять блоки памяти (намного быстрее, чем когда-либо мог бы ЦП). Я сделал это на процессоре Freescale i.MX21.

person Greg Hewgill    schedule 22.12.2008

Это можно сделать в сборке x86 так же легко. Фактически, это сводится к почти идентичному коду вашего примера.

mov esi, source    ; set esi to be the source
lea edi, [esi + 1] ; set edi to be the source + 1
mov byte [esi], 0  ; initialize the first byte with the "seed"
mov ecx, 100h      ; set ecx to the size of the buffer
rep movsb          ; do the fill

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

Наконец, memcpy/memmove - это не то, что вам нужно, они предназначены для копирования блоков памяти из одной области в другую (memmove позволяет источнику и месту назначения быть частью одного и того же буфера). memset заполняет блок байтом по вашему выбору.

person Evan Teran    schedule 23.12.2008
comment
На x86 rep stosd с ecx=40h будет НАМНОГО быстрее, и я думаю, именно поэтому мы должны избегать хаков, а вместо этого придерживаться простого вызова memset() :) - person Sedat Kapanoglu; 03.02.2011
comment
@ssg: да, rep stosd было бы более эффективно, но я пытался продемонстрировать код, который действовал так же, как OP. Я также отметил в своем посте, что установка более одного байта за раз была бы более эффективной. - person Evan Teran; 03.02.2011
comment
да, у меня было такое предчувствие. Я ориентировался на точку зрения ОП, а не на вашу :) - person Sedat Kapanoglu; 03.02.2011

Существует также calloc, который выделяет и инициализирует память до 0 перед возвратом указатель. Конечно, calloc инициализируется только 0, а не тем, что указывает пользователь.

person codelogic    schedule 22.12.2008

Если это наиболее эффективный способ установить для блока памяти заданное значение на Z80, то вполне возможно, что memset() может быть реализовано, как вы описываете, в компиляторе, ориентированном на Z80.

Возможно, memcpy() также может использовать аналогичную последовательность в этом компиляторе.

Но почему компиляторы, предназначенные для процессоров с совершенно отличными от Z80 наборами инструкций, должны использовать идиому Z80 для таких вещей?

Помните, что архитектура x86 имеет аналогичный набор инструкций, перед которыми может стоять код операции REP, чтобы они выполнялись повторно для выполнения таких действий, как копирование, заполнение или сравнение блоков памяти. Однако к тому времени, когда Intel выпустила 386 (или, может быть, это была 486), процессор фактически выполнял эти инструкции медленнее, чем более простые инструкции в цикле. Поэтому компиляторы часто отказывались от использования инструкций, ориентированных на REP.

person Michael Burr    schedule 23.12.2008

Серьезно, если вы пишете на C/C++, просто напишите простой цикл for и позвольте компилятору побеспокоиться за вас. В качестве примера, вот некоторый код VS2005, сгенерированный именно для этого случая (с использованием шаблонного размера):

template <int S>
class A
{
  char s_[S];
public:
  A()
  {
    for(int i = 0; i < S; ++i)
    {
      s_[i] = 'A';
    }
  }
  int MaxLength() const
  {
    return S;
  }
};

extern void useA(A<5> &a, int n); // fool the optimizer into generating any code at all

void test()
{
  A<5> a5;
  useA(a5, a5.MaxLength());
}

Вывод ассемблера следующий:

test PROC

[snip]

; 25   :    A<5> a5;

mov eax, 41414141H              ;"AAAA"
mov DWORD PTR a5[esp+40], eax
mov BYTE PTR a5[esp+44], al

; 26   :    useA(a5, a5.MaxLength());

lea eax, DWORD PTR a5[esp+40]
push    5               ; MaxLength()
push    eax
call    useA

Это не более эффективно. Перестаньте беспокоиться и доверьтесь своему компилятору или, по крайней мере, посмотрите, что выдает ваш компилятор, прежде чем пытаться найти способы оптимизации. Для сравнения я также скомпилировал код, используя std::fill(s_, s_ + S, 'A') и std::memset(s_, 'A', S) вместо цикла for, и компилятор выдал идентичный результат.

person Andreas Magnusson    schedule 23.12.2008
comment
Если этот вывод был из objedump, вы должны передать параметр -C, он будет декодировать имена С++ :) - person Evan Teran; 23.12.2008
comment
Спасибо, но вывод был напрямую из компилятора, я мог бы, конечно, немного его привести в порядок... - person Andreas Magnusson; 24.12.2008
comment
Ваш пример не является хорошим, потому что компилятор обнаруживает, что массив имеет только 5 байтов, так же как и операция сохранения 4 байта и 1 байта из eax. Это выглядело бы совершенно по-другому при использовании значительно большего размера массива. - person Razzupaltuff; 29.12.2008
comment
Конечно, но все дело было в том, чтобы проиллюстрировать, что навороченные методы оптимизации, использовавшиеся в былые времена для написания Z80 asm, больше не нужны. Использование большего значения для S приведет к вызову memset(), который, скорее всего, выполнит rep stosd (+ выравнивание). - person Andreas Magnusson; 31.12.2008

Если вы используете PowerPC, _dcbz().

person Crashworks    schedule 14.01.2009

Существует ряд ситуаций, когда было бы полезно иметь функцию «memsread», определяемое поведение которой состояло в том, чтобы копировать начальную часть диапазона памяти во всем этом. Хотя memset() прекрасно работает, если целью является распространение одного байтового значения, бывают случаи, когда, например. можно захотеть заполнить массив целых чисел одним и тем же значением. Во многих реализациях процессора копирование побайтов из источника в место назначения было бы довольно неудобным способом реализации, но хорошо спроектированная функция может дать хорошие результаты. Например, начните с того, что посмотрите, меньше ли объем данных 32 байта или около того; если это так, просто сделайте побайтовую копию; в противном случае проверьте выравнивание источника и назначения; если они выровнены, округлить размер в меньшую сторону до ближайшего слова (если необходимо), затем скопировать первое слово везде, где оно идет, скопировать следующее слово везде, где оно идет, и т. д.

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

person supercat    schedule 19.04.2011

memcpy() должно иметь такое поведение. memmove() не делает этого по дизайну, если блоки памяти перекрываются, он копирует содержимое, начиная с концов буферов, чтобы избежать такого поведения. Но чтобы заполнить буфер определенным значением, вы должны использовать memset() в C или std::fill() в C++, которые большинство современных компиляторов оптимизируют для соответствующей инструкции заполнения блока (например, REP STOSB на архитектурах x86).

person Ferruccio    schedule 23.12.2008
comment
Почему у memcpy такое поведение? На большинстве аппаратных средств я был бы глубоко разочарован memcpy настолько неоптимизированным, что он, по сути, принимает и записывает по одному байту за раз, на что опирается это использование LDIR, но стандартные функции C не предлагают. - person Steve Jessop; 23.12.2008

Как было сказано ранее, memset() предлагает желаемую функциональность.

memcpy() предназначена для перемещения по блокам памяти во всех случаях, когда исходный и целевой буферы не перекрываются, или когда место назначения ‹ источник.

memmove() решает случай перекрытия буферов и назначения > источника.

В архитектурах x86 хорошие компиляторы напрямую заменяют вызовы memset встроенными инструкциями по ассемблеру, очень эффективно устанавливая память целевого буфера, даже применяя дальнейшие оптимизации, такие как использование 4-байтовых значений для заполнения как можно дольше (если следующий код не полностью синтаксически верен, виноват это из-за того, что я давно не использую ассемблерный код X86):

lea edi,dest
;copy the fill byte to all 4 bytes of eax
mov al,fill
mov ah,al
mov dx,ax
shl eax,16
mov ax,dx
mov ecx,count
mov edx,ecx
shr ecx,2
cld
rep stosd
test edx,2
jz moveByte
stosw
moveByte:
test edx,1
jz fillDone
stosb
fillDone:

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

Если компилятор наполовину хорош, он может обнаружить более сложный код C++, который можно разбить на memset (см. пост ниже), но я сомневаюсь, что это действительно происходит с вложенными циклами, возможно, даже с вызовом функций инициализации.

person Razzupaltuff    schedule 29.12.2008