Pregunta

En el código máquina Z80, una técnica barata para inicializar un búfer a un valor fijo, dicen todos los espacios en blanco. Por lo que un trozo de código podría ser algo como esto.

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

El resultado es que el trozo de memoria en el destino está completamente en blanco lleno. He experimentado con memmove, y establecimiento de memoria, y no puedo reproducir este comportamiento. Memmove que esperaba para ser capaz de hacerlo correctamente.

¿Por qué memmove y memcpy comportarse de esta manera?

¿Hay alguna forma razonable de hacer este tipo de inicialización del campo?

Ya soy consciente de matriz de caracteres [tamaño] = {0} para array inicialización

Ya soy consciente de que memset hará el trabajo para los caracteres individuales.

Lo que otros enfoques están ahí para este problema?

¿Fue útil?

Solución

Creo que esto va a la filosofía de diseño de C y C ++. Como Bjarne Stroustrup una vez dijo , uno de los principios rectores del diseño de C ++ es 'lo que no se utiliza, no se paga'. Y mientras Dennis Ritchie pueden no haber dicho que en exactamente las mismas palabras, creo que era un guiado principio de informar a su diseño de C (y el diseño de C por personas posteriores) también. Ahora usted puede pensar que si asigna automáticamente la memoria debe ser inicializado a NULL de y me tienden a estar de acuerdo con usted. Pero eso toma ciclos de máquina y si usted está codificando en una situación en la que cada ciclo es fundamental, que no puede haber un compromiso aceptable. Básicamente C y C ++ tratan de mantenerse fuera de su camino - por lo tanto, si quieres algo inicializado que tiene que hacer usted mismo.

Otros consejos

memmove y memcpy no funcionan de esa manera porque no es una semántica útil para mover o copiar la memoria. Es muy útil en el Z80 a no ser capaz de llenar la memoria, pero ¿por qué se puede esperar una función llamada "memmove" para llenar la memoria con un solo byte? Es para mover los bloques de memoria alrededor. Se implementa para obtener la respuesta correcta (los bytes de origen se mueven al destino), independientemente de cómo se superponen los bloques. Es útil para que llegue la respuesta correcta para mover los bloques de memoria.

Si desea llenar la memoria, utilice memset, que está diseñado para hacer exactamente lo que quiere.

Hubo una manera más rápida de obturación un área de memoria utilizando la pila. Aunque el uso de LDI y LDIR era muy común, David Webb (que empujó el ZX Spectrum en todo tipo de formas como cuentas atrás número de pantalla completa, incluyendo la frontera) se le ocurrió esta técnica que es 4 veces más rápido:

  • guarda el puntero de pila y luego mueve al final de la pantalla.
  • carga el par de registro HL con cero,
  • entra en un bucle masiva Empujar HL en la pila.
  • La pila se mueve hacia arriba y hacia abajo de la pantalla medio de la memoria y en el proceso, despeja la pantalla.

La explicación anterior fue tomada del href="http://www.users.globalnet.co.uk/~jg27paw4/yr15/yr15_36.htm" rel="nofollow opinión de David Webb juego starion .

La rutina Z80 podría parecer un poco como esto:

  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 obstante, esa rutina es un poco menos de dos veces más rápido. copias LDIR un byte cada 21 ciclos. Las copias de bucle interiores dos bytes cada 24 ciclos - 11 ciclos para PUSH HL y 13 para DJNZ LOOP. Para obtener casi 4 veces más rápido simplemente desenrollar el bucle interno:

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

Esto es muy cerca de 11 ciclos cada dos bytes que es aproximadamente 3,8 veces más rápido que los 21 ciclos por byte de LDIR.

Sin duda, la técnica se ha reinventado muchas veces. Por ejemplo, se apareció anteriormente en de sub-Logic simulador de vuelo 1 para el TRS-80 en 1980.

  

¿Por qué memmove y memcpy comportarse de esta manera?

Probablemente porque no hay moderna ++ Compiler específica, C que se dirige el hardware Z80? Escribir uno. ; -)

Las lenguas no especifican cómo un determinado hardware implementa nada. Esto es totalmente de los programadores del compilador y bibliotecas. Por supuesto, escribir una propia versión, altamente especifica para cada configuración de hardware es imaginable un montón de trabajo. Esa será la razón.

  

¿Hay alguna forma razonable de hacer este tipo de inicialización del campo? ¿Hay alguna forma razonable de hacer este tipo de inicialización del campo?

Bueno, si todo lo demás falla siempre se puede usar ensamblador en línea. Aparte de eso, espero std::fill para llevar a cabo mejor en una buena aplicación STL. Y sí, soy plenamente consciente de que mis expectativas son demasiado altas y que std::memset menudo funciona mejor en la práctica.

La secuencia Z80 mostrar era la forma más rápida de hacerlo - en 1978. Eso fue hace 30 años. Los procesadores han progresado mucho desde entonces, y hoy que se trata sólo de la manera más lenta para hacerlo.

memmove está diseñado para trabajar cuando los rangos de origen y de destino se solapan, por lo que puede mover un trozo de memoria por un byte. Eso es parte de su comportamiento especificado por el C y estándares de C ++. Memcpy es no especificado; Puede que funcione de forma idéntica a memmove, o podría ser diferente, dependiendo de cómo el compilador decide ponerlo en práctica. El compilador es libre de elegir un método que es más eficiente que memmove.

Si usted está jugando a nivel de hardware, a continuación, algunas CPUs tienen controladores de DMA que pueden llenar bloques de memoria muy rápidamente (mucho más rápido que la CPU no podrían hacerlo). He hecho esto en una CPU Freescale i.MX21.

Esto se logra en conjunto x86 misma facilidad. De hecho, todo se resume en código casi idéntico a su ejemplo.

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

Sin embargo, es simplemente más eficiente para establecer más de un byte a la vez si es posible.

Por último, memcpy / memmove no son lo que busca, esos son para hacer copias de bloques de memoria a partir del área a otra (memmove permite fuente y dest para formar parte del mismo tampón). memset llena un bloque con un byte de su elección.

También hay calloc que asigna e inicializa la memoria a 0 antes de devolver el puntero. Por supuesto, sólo se calloc inicializa a 0, no es algo que el usuario especifica.

Si esta es la forma más eficaz para establecer un bloque de memoria a un valor dado en el Z80, entonces es muy posible que memset() podría implementarse como usted la describe en un compilador que se dirige a Z80s.

Podría ser que memcpy() también podría usar una secuencia similar en ese compilador.

Pero ¿por qué se esperaría que los compiladores de orientación CPU completamente diferentes conjuntos de instrucciones del Z80 utilizar un lenguaje Z80 para este tipo de cosas?

Recuerde que la arquitectura x86 tiene un conjunto similar de instrucciones que podrían ser precedido de un código de operación REP tenerlos ejecutar varias veces para hacer las cosas como copiar, rellenar o comparar bloques de memoria. Sin embargo, en el momento en Intel salió con el 386 (o tal vez fue el 486) de la CPU en realidad ejecutar esas instrucciones más lentas que las instrucciones más sencillas en un bucle. Así compiladores a menudo dejado de usar las instrucciones orientadas REP.

En serio, si usted está escribiendo en C / C ++, acaba de escribir un simple bucle for y dejar que el compilador de molestia para usted. A modo de ejemplo, he aquí algunos VS2005 código generado para este caso exacto (con el tamaño de plantilla):

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

La salida ensamblador es la siguiente:

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

Lo hace no conseguir más eficiente que eso. Deje de preocuparse y confiar en que el compilador o al menos echar un vistazo a lo que su compilador produce antes de intentar encontrar maneras de optimizar. Para la comparación también Compilé el código usando std::fill(s_, s_ + S, 'A') y std::memset(s_, 'A', S) en lugar de la for-loop y el compilador producido la salida idénticas.

Si estás en el PowerPC, _dcbz ().

Hay una serie de situaciones en las que sería útil tener una función "memspread" cuyo comportamiento definido era copiar la parte inicial de un rango de memoria a lo largo de todo el asunto. Aunque memset () funciona muy bien si el objetivo es difundir un único valor de byte, hay momentos en que por ejemplo, uno puede querer llenar una matriz de enteros con el mismo valor. En muchas implementaciones de procesador, la copia de un byte a la vez desde el origen al destino sería una manera bastante terrible para ponerlo en práctica, sino una función bien diseñado podría dar buenos resultados. Por ejemplo, empezar por ver si la cantidad de datos es menor que 32 bytes o menos; Si es así, acaba de hacer una copia byte a byte; de lo contrario comprobar la alineación de la fuente y de destino; si están alineados, redondo del tamaño abajo a la palabra más cercana (si es necesario), y luego copiar la primera palabra todas partes que va, copie la siguiente palabra todas partes que va, etc.

I también tengo a veces desean para una función que se ha especificado para trabajar como memcpy de abajo hacia arriba, destinado para su uso con intervalos que se solapan. En cuanto a por qué no hay una norma, supongo que nadie pensó que era importante.

memcpy() tenga ese comportamiento. memmove() no es así por diseño, si los bloques de pisos de memoria, copia el contenido a partir de los extremos de los amortiguadores para evitar ese tipo de comportamiento. Pero para llenar un tampón con un valor específico que debe utilizar memset() en C o std::fill() en C ++, que la mayoría de los compiladores modernos optimizarán a la instrucción de relleno bloque apropiado (como REP STOSB en arquitecturas x86).

Como se dijo antes, memset () Ofrece la funcionalidad deseada.

memcpy () es para moverse por bloques de memoria en todos los casos en los buffers de origen y de destino no se superponen, o donde dest

memmove () resuelve el caso de tampones superpuestos y dest> fuente.

En las arquitecturas x86, buenos compiladores reemplazar directamente las llamadas Memset con instrucciones de montaje en línea de ajuste de manera muy eficaz la memoria del búfer de destino, incluso aplicando optimizaciones adicionales como el uso de valores de 4 bytes para llenar el mayor tiempo posible (si el siguiente código no es totalmente sintácticamente correcta culpa en mi no usar código ensamblador X86 durante mucho tiempo):

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:

En realidad, este código es mucho más eficiente que la versión Z80, ya que no hace memoria a la memoria, pero sólo se registra a la memoria se mueve. Su código Z80 es, de hecho, un buen truco ya que depende de cada operación de copia de haber llenado la fuente de la copia posterior.

Si el compilador está a medio camino bueno, podría ser capaz de detectar más complicado código C ++ que se puede dividir a memset (ver el post de abajo), pero no creo que esto realmente sucede por bucles anidados, probablemente incluso invocar funciones de inicialización .

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top