Почему в C/C ++/RTL нет функциональности LDIR?

StackOverflow https://stackoverflow.com/questions/387654

  •  23-08-2019
  •  | 
  •  

Вопрос

В машинном коде 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...

Результатом является то, что кусок памяти в пункте назначения полностью заполнен. Я экспериментировал с Memmove и Memcpy, и не могу воспроизвести это поведение. Я ожидал, что Memmove сможет сделать это правильно.

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

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

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

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

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

Это было полезно?

Решение

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

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

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

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

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

  • Сохраняет указатель стека, а затем перемещает его к концу экрана.
  • Загружает пару регистра HL с ноль,
  • Входит в массивный цикл, толкая HL на стек.
  • Стек перемещается вверх по экрану и вниз по памяти и в процессе очищает экран.

Приведенное выше объяснение было взято из Обзор David Webbs Game 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.

Несомненно, техника была заново изобретана много раз. Например, он появился ранее в Sub-Logic's Flight Simulator 1 для TRS-80 в 1980 году.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Если вы находитесь на PowerPC, _dcbz ().

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

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

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

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

Memcpy () предназначен для перемещения блоков памяти во всех случаях, когда буферы источника и назначения не перекрываются или где Dest <Source.

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top