Почему в C / C ++ / rtl нет Z80, подобного функциональности LDIR? - PullRequest
5 голосов
/ 23 декабря 2008

В машинном коде 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 [size] = {0} для инициализации массива

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

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

Ответы [ 14 ]

12 голосов
/ 23 декабря 2008

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

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

11 голосов
/ 23 декабря 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.

Несомненно, эта техника многократно переосмысливалась. Например, он появился ранее в Flight Logulator 1 для TRS-80 в 1980 году.

8 голосов
/ 23 декабря 2008

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

6 голосов
/ 23 декабря 2008

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

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

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

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

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

5 голосов
/ 23 декабря 2008

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

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

4 голосов
/ 23 декабря 2008

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

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

2 голосов
/ 20 апреля 2011

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

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

2 голосов
/ 14 января 2009

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

2 голосов
/ 23 декабря 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, и компилятор выдал идентичный вывод.

...