¿Diferencia entre el código ensamblador generado para uint y uint32?

Escribí un contrato simple con una sola variable de estado con tipo uinty el código ensamblador generado es bastante sencillo. Pero cuando cambié el tipo al uint32ensamblaje generado se vuelve completamente diferente. Además uint32, si marco la opción 'Habilitar Optimizador', el código de ensamblaje generado se modifica.

Preguntas:

  1. En el caso de uint32, ¿por qué se cambia el ensamblaje generado? Además, si puede, explique el ensamblaje generado en sí.
  2. Los códigos de ensamblaje para uint32 con el optimizador activado y desactivado son diferentes. ¿Por que es esto entonces? Entiendo que el optimizador está optimizando el rendimiento, por eso las instrucciones de montaje se están haciendo más pequeñas, pero mi pregunta es ¿cómo?

Para su referencia, proporciono el contrato junto con el segmento de código ensamblador generado.

Contrato

pragma solidity ^0.4.0;

contract Test 
{
   uint32 value = 10;
}

Ensamblaje generado con el optimizador desactivado

PUSH1 0x60 
PUSH1 0x40 
MSTORE 
PUSH1 0xA 
PUSH1 0x0 
PUSH1 0x0 
PUSH2 0x100 
EXP 
DUP2 
SLOAD 
DUP2 
PUSH4 0xFFFFFFFF 
MUL 
NOT 
AND 
SWAP1 
DUP4 
PUSH4 0xFFFFFFFF 
AND 
MUL 
OR 
SWAP1 
SSTORE 
POP 
CALLVALUE 
PUSH1 0x0 
JUMPI 

Ensamblaje generado con optimizador activado

PUSH1 0x60
PUSH1 0x40
MSTORE
PUSH1 0x0
DUP1
SLOAD
PUSH4 0xFFFFFFFF
NOT
AND
PUSH1 0xA
OR
SWAP1
SSTORE
CALLVALUE
PUSH1 0x0
JUMPI 

Gracias, Shamik.

Hola. Me pregunto por qué necesita saber esto, ya que se hacen todos los esfuerzos para evitar este código de muy bajo nivel mediante la creación de algún tipo de interfaz como ABI o bytecode.
Estábamos tratando de investigar los códigos de operación generados y encontramos este segmento. ¿Puede explicar por qué funciona de esta manera?
uintes equivalente a uint256pero incluso en ese caso el bytecode generado todavía parece diferente, interesante...
Hemos entendido el mecanismo de almacenamiento cuando el optimizador está activado. Pero, ¿por qué se requiere este SLOAD al principio? Según la especificación, el almacenamiento tiene una longitud de 32 bytes (tanto clave como valor) y el valor predeterminado es todo ceros.

Respuestas (1)

Otros han apuntado en la dirección correcta, pero permítanme tratar de responder específicamente a las preguntas.

En primer lugar, cada palabra de 256 bits de almacenamiento por contrato es muy costosa. Cuando un contrato utiliza variables más pequeñas, como uint32 (32 bits), Solidity intentará empaquetar múltiples variables en una sola palabra de almacenamiento. La mayor parte de lo que ve aquí es que el compilador primero inserta el aparato para empaquetar y desempaquetar las variables uint32 del almacenamiento, y luego el optimizador lo extrae todo nuevamente, ya que no es necesario en este caso realmente simple.

Etiquetas útiles

Voy a definir algunas etiquetas para hacer un seguimiento de las cosas:

VALUE = value, the uint32 in your contract = 0x0a (ten)
MASK  = 0xFFFFFFFF This is 32 bits of ones, and matches the width of a uint32.
S[0]  = the contract storage location zero: 256 bits wide.
WORD  = the contents of S[0]: potentially packed storage of uint32s
N     = the shift: number of bytes from the right-hand-side of WORD where VALUE is stored within S[0]

Gráficamente, aquí hay un ejemplo de 32 bytes de S[0] WORD, con el VALOR de almacenamiento marcado con VVVV, desplazado por N bytes (12 aquí) dentro de S[0].

0123456789abcdef0123456789abcdef
................VVVV............
                    <---- N ----

Q1 en detalle

Primero, es solo el preámbulo de administración de memoria que el compilador siempre inserta. La parte superior de la memoria utilizada es inicialmente 0x60 y este valor se almacena en 0x40 para referencia posterior:

PUSH1 0x60 
PUSH1 0x40 
MSTORE

El siguiente es el valor de su asignación, 10 enuint32 value = 10;

PUSH1 0xA     // VALUE

Ahora, el compilador sabe que tiene que almacenar la variable y que es más corta que una palabra completa: un uint32 en lugar de 256 bits. Por lo tanto, crea una máscara y desplaza la máscara en N bytes hasta donde se almacena la variable en la palabra de 256 bits. Ignora el hecho de que N=0 por ahora; podría ser diferente de 0 en general.

PUSH1 0x0     // the zero in S[0].
PUSH1 0x0     // the shift, N
PUSH2 0x100   // One byte shift left is 8 bits
EXP           // Calculates 0x100 ^ N, i.e. 8*N-bits shifter
DUP2          // Get the storage slot number [0]
SLOAD         // Load WORD from S[0]
DUP2          // Get the shifter we calculated earlier
PUSH4 0xFFFFFFFF // MASK
MUL           // Shift the mask along by N bytes
NOT           // Invert the mask - every bit is flipped.
AND           // Apply the mask. All bits of WORD within the 32 bits of your variable are set to zero; all bits outside are unaffected.

Ahora tenemos la PALABRA original de S[0] en la memoria, con la ubicación de VALOR en cero. Debe ponerse a cero, ya que no podemos confiar en que sea cero si se ha establecido mediante una invocación previa del contrato.

SWAP1         // Retrieve the shifter we calculated earlier
DUP4          // VALUE is retrieved
PUSH4 0xFFFFFFFF // MASK again
AND           // Ensure VALUE is truncated to 32 bits since it is uint32
MUL           // Shift value left by N bytes
OR            // logical or VALUE into WORD. The corresponding WORD bits are zero, so this just sets them to VALUE
SWAP1 
SSTORE        // Store WORD back into memory at S[0]

Ahora hemos terminado, con los 32 bits de VALUE insertados en el lugar correcto en S[0] y todos los demás bits de S[0] no afectados.

Optimización Q2

Ahora, lo anterior describe el caso general para cualquier cambio, N. Sin embargo, en este caso N==0 y esa es una situación mucho más fácil, por lo que el optimizador puede encontrar algunas simplificaciones.

  1. El optimizador reconoce que 0x100 ^ 0 = 1, y que multiplicar por 1 no funciona. Por lo tanto, elimina todo el código asociado con el cambio de MÁSCARA o VALOR. No se requiere turno en esta ocasión ya que solo tenemos una variable.

  2. El optimizador también reconoce que el VALOR de la pila insertada (0x0a) tiene menos de 32 bits de ancho, por lo que no necesita aplicarle MÁSCARA.

Esto es suficiente para producir el código optimizado final. Hace un buen trabajo en este caso y produce un código mucho más limpio.