Pergunta

No código da máquina Z80, uma técnica barata para inicializar um buffer com um valor fixo, digamos todos os espaços em branco. Portanto, um pedaço de código pode parecer algo assim.

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...

O resultado é que o pedaço da memória no destino está completamente em branco preenchido. Eu experimentei Memmove e Memcpy e não consigo replicar esse comportamento. Eu esperava que o Memmove pudesse fazê -lo corretamente.

Por que Memmove e Memcpy se comportam dessa maneira?

Existe alguma maneira razoável de fazer esse tipo de inicialização da matriz?

Já estou ciente da matriz de char [size] = {0} para inicialização da matriz

Já estou ciente de que o MEMSET fará o trabalho para caracteres únicos.

Que outras abordagens existem para esse problema?

Foi útil?

Solução

Eu acredito que isso vai para a filosofia de design de C e C ++. Como Bjarne Stroustrup uma vez disse, um dos principais princípios orientadores do design do C ++ é "o que você não usa, não paga". E enquanto Dennis Ritchie Pode não ter dito exatamente nessas mesmas palavras, acredito que esse foi um princípio orientador informando seu design de C (e o design de C pelas pessoas subsequentes) também. Agora você pode pensar que, se alocar a memória, ela deve ser automaticamente inicializada para a Null e eu tenderia a concordar com você. Mas isso leva ciclos de máquina e, se você está codificando em uma situação em que cada ciclo é crítico, isso pode não ser uma troca aceitável. Basicamente, C e C ++ tentam ficar de fora do seu caminho-se você quiser algo inicializado, precisa fazer isso sozinho.

Outras dicas

memmove e memcpy Não funcione dessa maneira, porque não é uma semântica útil para mover ou copiar a memória. É útil no Z80 ser capaz de preencher a memória, mas por que você esperaria que uma função chamada "Memmove" preencha a memória com um único byte? É para mover blocos de memória. É implementado para obter a resposta certa (os bytes de origem são movidos para o destino), independentemente de como os blocos se sobrepõem. É útil obter a resposta certa para mover blocos de memória.

Se você deseja preencher a memória, use o MEMSET, que foi projetado para fazer exatamente o que deseja.

Havia uma maneira mais rápida de bloquear uma área de memória usando a pilha. Embora o uso de LDI e LDIR tenha sido muito comum, David Webb (que empurrou o espectro ZX de todos os tipos de maneiras, como contagens de número de tela inteira, incluindo a fronteira), criou esta técnica 4 vezes mais rápida:

  • salva o ponteiro da pilha e depois move -o para o final da tela.
  • Carrega o par de registros HL com zero,
  • entra em um loop enorme empurrando HL para a pilha.
  • A pilha move a tela e para baixo através da memória e, no processo, limpa a tela.

A explicação acima foi tirada do Revisão de David Webbs Game Starion.

A rotina Z80 pode parecer um pouco assim:

  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

No entanto, essa rotina é um pouco menor duas vezes mais rápida. O LDIR copia um byte a cada 21 ciclos. O loop interno copia dois bytes a cada 24 ciclos - 11 ciclos para PUSH HL e 13 para DJNZ LOOP. Para obter quase 4 vezes mais rápido, simplesmente desenrole o loop interno:

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

São quase 11 ciclos a cada dois bytes, que são cerca de 3,8 vezes mais rápido que os 21 ciclos por byte do LDIR.

Sem dúvida, a técnica foi reinventada muitas vezes. Por exemplo, apareceu anteriormente em Simulador de voo da sub-logic 1 para o TRS-80 em 1980.

Por que Memmove e Memcpy se comportam dessa maneira?

Provavelmente porque não há compilador C ++ moderno específico que visa o hardware Z80? Escreva um. ;-)

Os idiomas não especificam como um determinado hardware implementa qualquer coisa. Isso depende inteiramente dos programadores do compilador e das bibliotecas. Obviamente, escrever uma versão altamente especificada para cada configuração de hardware imaginável é muito trabalho. Essa será a razão.

Existe alguma maneira razoável de fazer esse tipo de inicialização da matriz? Existe alguma maneira razoável de fazer esse tipo de inicialização da matriz?

Bem, se tudo mais falhar, você sempre poderá usar a montagem embutida. Fora isso, espero std::fill para executar o melhor em uma boa implementação do STL. E sim, estou plenamente ciente de que minhas expectativas são muito altas e que std::memset Freqüentemente tem um desempenho melhor na prática.

A sequência Z80 que você mostra foi a maneira mais rápida de fazer isso - em 1978. Isso foi há 30 anos. Os processadores progrediram muito desde então, e hoje essa é a maneira mais lenta de fazê -lo.

O Memmove foi projetado para funcionar quando a fonte e os intervalos de destino se sobrepõem, para que você possa mover um pedaço de memória para um byte. Isso faz parte de seu comportamento especificado pelos padrões C e C ++. Memcpy não é especificado; Pode funcionar de forma idêntica ao Memmove, ou pode ser diferente, dependendo de como seu compilador decide implementá -lo. O compilador é livre para escolher um método mais eficiente que o Memmove.

Se você estiver brincando no nível de hardware, algumas CPUs têm controladores DMA que podem preencher blocos de memória muito rapidamente (muito mais rápido que a CPU poderia fazer). Eu fiz isso em uma CPU I.MX21 de freescale.

Isso será realizado na montagem x86 com a mesma facilidade. De fato, tudo se resume a um código quase idêntico ao seu exemplo.

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

No entanto, é simplesmente mais eficiente definir mais de um byte de cada vez, se puder.

Finalmente, memcpy/memmove Não são o que você está procurando, esses são para fazer cópias de blocos de memória da área para outra (o Memmove permite que a fonte e o destem façam parte do mesmo buffer). memset Preenche um bloco com um byte de sua escolha.

Há também Calloc Isso aloca e inicializa a memória para 0 antes de retornar o ponteiro. Obviamente, o Calloc inicializa apenas para 0, não algo que o usuário especifica.

Se essa é a maneira mais eficiente de definir um bloco de memória como um determinado valor no Z80, é bem possível que memset() Pode ser implementado como você descreve em um compilador que tem como alvo o Z80S.

Pode ser isso memcpy() Também pode usar uma sequência semelhante nesse compilador.

Mas por que os compiladores direcionam as CPUs com conjuntos de instruções completamente diferentes do Z80 deveriam usar um idioma Z80 para esses tipos de coisas?

Lembre -se de que a arquitetura X86 possui um conjunto semelhante de instruções que podem ser prefixadas com um código de representação para que eles sejam executados repetidamente para fazer coisas como copiar, preencher ou comparar blocos de memória. No entanto, quando a Intel saiu com o 386 (ou talvez fosse o 486), a CPU realmente executava essas instruções mais lentas que as instruções mais simples em um loop. Portanto, os compiladores geralmente pararam de usar as instruções orientadas para o representante.

Sério, se você está escrevendo C/C ++, apenas escreva um loop simples e deixe o compilador incomodar para você. Como exemplo, aqui está algum código vs2005 gerado para este caso exato (usando tamanho modificado):

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());
}

A saída do assembler é o seguinte:

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

Isso faz não fique mais eficiente do que isso. Pare de se preocupar e confie no seu compilador ou, pelo menos, dê uma olhada no que o seu compilador produz antes de tentar encontrar maneiras de otimizar. Para comparação, também compilei o código usando std::fill(s_, s_ + S, 'A') e std::memset(s_, 'A', S) em vez do loop for e o compilador produziu a saída idêntica.

Se você estiver no PowerPC, _dcbz ().

Há várias situações em que seria útil ter uma função "MemsPread" cujo comportamento definido era copiar a parte inicial de uma faixa de memória em toda a coisa. Embora o MEMSET () seja bom se o objetivo é espalhar um único valor de byte, há momentos em que, por exemplo, alguém pode querer preencher uma variedade de números inteiros com o mesmo valor. Em muitas implementações de processador, copiar um byte de uma época da fonte para o destino seria uma maneira bastante ruim de implementá-lo, mas uma função bem projetada poderia produzir bons resultados. Por exemplo, comece vendo se a quantidade de dados é inferior a 32 bytes; Nesse caso, basta fazer uma cópia bytewise; Caso contrário, verifique o alinhamento de origem e destino; Se eles estiverem alinhados, ao redor do tamanho até a palavra mais próxima (se necessário), copie a primeira palavra em todos os lugares que vai, copie a próxima palavra em todos os lugares que vai, etc.

Às vezes também desejei uma função especificada para funcionar como um memcpy de baixo para cima, pretendido para uso com intervalos sobrepostos. Quanto ao motivo pelo qual não há um padrão, acho que ninguém achou importante.

memcpy() deve ter esse comportamento. memmove() Por design, se os blocos de memória se sobreporem, ele copia o conteúdo que inicia nas extremidades dos buffers para evitar esse tipo de comportamento. Mas para preencher um buffer com um valor específico que você deve usar memset() em c ou std::fill() Em C ++, que a maioria dos compiladores modernos otimizará para a instrução de preenchimento de bloco apropriada (como o REP STOSB nas arquiteturas x86).

Como dito antes, o MEMSET () oferece a funcionalidade desejada.

Memcpy () é para mover -se em torno de blocos de memória em todos os casos em que os buffers de origem e destino não se sobrepõem ou onde destinam -se.

MemMove () resolve o caso de buffers sobrepostos e destinatários.

Nas arquiteturas x86, bons compiladores substituem diretamente as chamadas do MEMSET por instruções de montagem em linha definindo com muita eficácia a memória do buffer de destino, até aplicando otimizações adicionais como usar valores de 4 bytes para preencher o maior tempo possível (se o código a seguir não for totalmente sintaticamente correto Não estou usando o código de montagem X86 por um longo tempo):

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:

Na verdade, esse código é muito mais eficiente que a sua versão Z80, pois não faz memória na memória, mas apenas registra os movimentos de memória. Seu código Z80 é de fato um hack, pois se baseia em cada operação de cópia, tendo preenchido a fonte da cópia subsequente.

Se o compilador estiver no meio do caminho, poderá detectar um código C ++ mais complicado que pode ser dividido no Memset (veja a postagem abaixo), mas duvido que isso realmente aconteça para loops aninhados, provavelmente até invocando funções de inicialização.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top