Почему в 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 выполнит эту работу для отдельных символов.

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
0
2 993
14
Перейти к ответу Данный вопрос помечен как решенный

Ответы 14

Why do memmove and memcpy behave this way?

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

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

Is there any reasonable way to do this sort of array initialization?Is there any reasonable way to do this sort of array initialization?

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

Я не ищу компилятор z80. Я ищу и ldir-подобный метод для инициализации буферов.

EvilTeach 23.12.2008 06:17

Я не ответил, так как не понимаю вопроса. Способ инициализации буферов в C++ - std :: fill (или memset, или wmemset, или непереносимые эквиваленты для больших значений). Почему тебе это не нравится? Что мотивирует требование «должно быть LDIR-подобным», вы просто любите идиому?

Steve Jessop 23.12.2008 21:45

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

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

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

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

Ответ принят как подходящий

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

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

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

Steve Jessop 23.12.2008 21:34

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

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

Это так же легко сделать и в сборке 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 позволяет source и dest быть частью одного и того же буфера). memset заполняет блок выбранным вами байтом.

На x86 rep stosd с ecx=40h будет НАМНОГО быстрее, и я думаю, поэтому нам следует избегать взломов, а вместо этого придерживаться простого вызова memset() :)

Sedat Kapanoglu 03.02.2011 15:55

@ssg: да, rep stosd был бы более эффективным, но я пытался продемонстрировать код, который действует так же, как OP. Я также отметил в своем сообщении, что установка более одного байта за раз будет более эффективной.

Evan Teran 03.02.2011 18:52

да, у меня было такое предчувствие. Я ориентировался на точку зрения ОП, а не на вашу :)

Sedat Kapanoglu 03.02.2011 22:05

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

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

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

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

Был более быстрый способ очистить область памяти с помощью стека. Хотя использование 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.

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

Прошло много лет с тех пор, как я сделал что-нибудь с Z80, но мне это нравится. Конечно, я бы добавил в конце «LD SP, DE».

David Thornley 24.12.2008 01:39

Еще более быстрый способ, который я использовал, - это поместить в цикл несколько инструкций «PUSH HL». Итак, если вы очищали, скажем, 2 КБ памяти, вы можете использовать 16 «PUSH HL» и только выполнить цикл около 2 КБ / 16 (256) раз.

Mike Thompson 09.01.2009 09:07

DEC не устанавливает нулевой флаг. Фактически это не устанавливает никакого флага.

Sedat Kapanoglu 02.03.2009 10:10

16-битное DEC не устанавливает никаких флагов, но 8-битное DEC устанавливает. Переписывание цикла на внутренний цикл по C и внешний цикл по B решит эту проблему, как и использование DJNZ, который на IIRC быстрее, чем DEC B; JNZ LOOP отдельно. Конечно, для этого потребуется, чтобы внутренний цикл был над B ...

RBerteig 15.04.2009 00:04

Как сказал @RBerteig, 16-битные инструкции DEC не влияют на флаги, поэтому должен использоваться 8-битный счетчик (или вложенный цикл с 8-битными счетчиками, если необходимо). Кроме того, я бы добавил DI в начале и EI в конце, чтобы не прерывать прерывания.

Anders Marzi Tornblad 01.07.2016 17:02

Серьезно, если вы пишете 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, и компилятор выдал идентичный результат.

Если этот вывод был из objedump, вы должны передать параметр -C, он будет декодировать имена C++ :)

Evan Teran 23.12.2008 20:41

Спасибо, но вывод был прямо из компилятора, я, конечно, мог бы немного его привести в порядок ...

Andreas Magnusson 24.12.2008 01:16

Ваш пример не очень хороший, потому что компилятор обнаруживает, что массив имеет только 5 байтов, как и 4-байтовая и 1-байтовая операция сохранения из eax. Это выглядело бы иначе при использовании массива значительно большего размера.

karx11erx 29.12.2008 19:16

Конечно, но вся суть заключалась в том, чтобы проиллюстрировать, что изящные методы оптимизации, использовавшиеся в былые времена при написании Z80 asm, больше не нужны. Использование большего значения для S приведет к вызову memset (), который, скорее всего, выполнит rep stosd (+ выравнивание).

Andreas Magnusson 31.12.2008 01:00

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

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

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

На архитектурах 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 (см. Сообщение ниже), но я сомневаюсь, что это действительно происходит для вложенных циклов, возможно, даже с вызовом функций инициализации.

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

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

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

Другие вопросы по теме