Restablecer la ranura de almacenamiento aumenta el uso de gas, aunque debería disminuirlo

Tengo un contrato simple que elimina el último elemento de la matriz:

pragma solidity^0.4.11;

contract GasRefundTest {

    uint[] myArray = [1, 2];

    function deleteLastElem() public returns(bytes32) {
        myArray.length--;
    }
}

El costo de transacción para llamar deleteLastElem()es 17182 gas.

Cuando lo cambio a:

pragma solidity^0.4.11;

contract GasRefundTest {

    uint[] myArray = [1, 2];

    function deleteLastElem() public returns(bytes32) {
        delete myArray[1];
        myArray.length--;
    }
}

el costo de transacción se convierte en 22480 gas.

Pensé que eliminar las ranuras de almacenamiento debería dar como resultado un reembolso de gasolina, en cambio, veo un aumento de gasolina.

¿Alguien puede explicar qué está pasando aquí?

Parece que la mejor manera de responder a esta pregunta es pasar a un nivel bajo y describir ambos procesos en detalle (código de bytes u códigos de operación).
Estoy de acuerdo. Mirar el código de bytes o el ensamblaje ayudaría a aclarar las cosas

Respuestas (2)

Reducir el tamaño de una matriz de tamaño dinámico ya pone a cero los elementos que se "eliminaron".

Entonces, la versión de su código que primero hace a delete myArray[1]solo está haciendo una escritura adicional en el almacenamiento que está a punto de realizarse de todos modos.

Cosas divertidas para probar:

// This doesn't take more gas depending on how big you make the array.
myArray.length = 3; // or 300 or 3000

myArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
...
// This takes more gas the more elements you're removing, because it has to zero
// out more positions in storage.
myArray.length = 9; // vs. 1

EDITAR

Debo señalar que este último ejemplo es un poco confuso. El reembolso del gas entra en vigor allí, pero está limitado a la mitad del gas consumido.

Gracias por la respuesta. Explique por qué el gas 17k es imposible. Vi en los comentarios que usted menciona que el gas pagado debe ser de al menos 21k, sin embargo, no encontré esto mencionado en ninguna parte del papel amarillo o en las fuentes. Por lo que entiendo, sin el reembolso, la transacción costaría 21k + 5k + 5k = ~ 31k (gas intrínseco + ajuste de longitud + ajuste del último elemento a 0). La mitad de eso es 15.5k. Después de reembolsar esta cantidad de gas consumido se convierte en 15500. Ronda sobre lo que muestra el remix.
En mi comentario anterior me refería a un límite de 15,5k, que es mayor que 15k, por lo que se reembolsarán los 15k, por un total de 16k.
@ medvedev1088 Estoy buscando una fuente, pero tampoco puedo encontrarla en el Libro Amarillo. Quizás me equivoque.
@ medvedev1088 Tienes razón. El único límite de reembolso es la mitad del gas consumido, por lo que es posible consumir menos de 21000 gas. (Observé transacciones reales en la red principal con menos de 21000 gas consumido).
Por cierto, el hecho de que la disminución de la longitud de la matriz sea el tiempo O(n) en lugar del tiempo constante esperado, y que el aumento de la longitud de la matriz sea el tiempo constante en lugar del O(n) esperado es contrario a la intuición y debe mencionarse en la documentación.
Debería ser la hora O(n), ¿verdad? No O(registro(n)).
Ah cierto, error tipográfico :)
Además, estoy de acuerdo en que esto debe mencionarse en la documentación. Tuve que experimentar para averiguar qué operación hizo la puesta a cero. (Supuse que uno tendría que hacerlo). Sin embargo, dado que todo el almacenamiento se inicializa implícitamente a cero, tiene sentido que esté acortando la matriz que tiene que hacer el trabajo. (La mayoría de las veces, sería un desperdicio no tener almacenamiento durante la expansión del arreglo).
También siento que restablecer las ranuras desocupadas no debería ser trabajo de un compilador. Será mejor que se implemente como un código de biblioteca (probablemente en ensamblaje). En muchos casos, la gente quiere controlar si y cómo poner a cero las ranuras (por ejemplo, si ya es 0, no lo toque).
Yo diría que es un buen comportamiento predeterminado que el almacenamiento se ponga a cero al reducir una matriz. (Cualquier otra cosa sería bastante sorprendente para el desarrollador). Siempre puede omitir la disminución myArray.lengthy, en su lugar, mantener su propia variable que indique cuántos elementos son válidos. Luego, podría implementar la lógica que desee sobre cuándo poner a cero las cosas.
Estoy de acuerdo en que debería ser el comportamiento predeterminado. Pasarlo del compilador a una biblioteca sería más transparente y flexible.
Por otro lado, la matriz es una estructura de datos integrada, por lo que tiene sentido que implementaron la mayoría de las operaciones en el compilador. Si es necesario, se puede implementar un contenedor como una biblioteca que tendría la lógica que describió anteriormente.
O en lugar de poner esta lógica en la propiedad de longitud, podrían agregar la función de cambio de tamaño para las matrices. De esta forma, queda más claro que no se trata solo de establecer la propiedad, sino también de todo el trabajo asociado con el cambio de tamaño de la matriz. Mucha gente no espera que la configuración de la longitud haga un trabajo pesado. En esta respuesta principal, por ejemplo, el autor no lo esperaba, así que limpió el espacio él mismo ethereum.stackexchange.com/a/1528/18932

Para explicar en el nivel alto, está haciendo 2 operaciones en lugar de 1, por lo tanto, requiere más gas. Todo el código que escribe se compila en comandos de Ethereum Virtual Machine (EVM) de bajo nivel, que luego son interpretados por ella. Para cada comando de este tipo hay un precio de gas particular definido, mire esto .

Ahora, en el segundo caso, usas delete. De los documentos ,

delete a asigna el valor inicial del tipo a un

Es importante tener en cuenta que eliminar a realmente se comporta como una asignación a a, es decir, almacena un nuevo objeto en a.

Por lo tanto, está poniendo 0allí primero, antes de disminuir la longitud de una matriz. Simplemente por interés, puede intentar usar en su myArray[1] = 0;lugar y ver cómo esto afecta el gas utilizado.

De la pregunta original: "Pensé que eliminar las ranuras de almacenamiento debería dar como resultado un reembolso de gasolina, en cambio, veo un aumento de gasolina". Vea mi respuesta de por qué este no es el caso aquí.
@smarx, vi tu respuesta y básicamente estamos hablando de lo mismo.
No creo que la confusión fuera sobre por qué hacer dos cosas cuesta más que una. Fue por eso que el reembolso del gas no está reduciendo el precio del gas.
@smarx, lo fue, y mi respuesta lo aclara tan bien como la tuya, ¿no es así?
Estoy completamente confundido ahora. No estoy seguro de si estoy malinterpretando tu respuesta o si estás malinterpretando la mía.
@smarx, mi respuesta sin el primer párrafo dice lo mismo que la tuya. El primer párrafo simplemente explica cómo funciona EVM en general para que el lector entienda por qué la asignación toma gasolina en lugar de reembolsarla. Tal vez, debería haber estructurado mi respuesta de una manera diferente para resolver la confusión.
La diferencia que veo es que su respuesta no aborda por qué escribir un cero no reduce el uso de gas como normalmente esperaría. ( foo = 6; foo = 0;es "2 operaciones en lugar de 1" pero consume menos gas que solo foo = 6;)
@smarx, pero tampoco veo su respuesta para abordar esto;) Con respecto a su ejemplo, sería bueno saber de usted por qué: ¿optimización del compilador? ¿Optimización del compilador + sin escritura real? (En caso de que foo fuera igual a 0 antes de la asignación)
Es la primera línea de mi respuesta: "Reducir el tamaño de una matriz de tamaño dinámico ya pone a cero los elementos que se 'eliminaron' myArray.length--" delete myArray[myArray.length - 1];. deleteno te gana nada.
Lo siento, estaba asumiendo en mi foo = 6; foo = 0;ejemplo que nofoo lo estaba ya . 0
Y no lo he probado con las optimizaciones del compilador habilitadas. Esperemos que el compilador no se moleste con el foo = 6. Mi punto era simplemente que "2 operaciones son más caras que 1" no es exacto debido a los reembolsos de gasolina. Quizás un ejemplo más claro sería foo = 6; bar = 0;consumir menos gas que solo foo = 6(de nuevo, donde barantes era distinto de cero).
Espera, ¿dijiste que sobrescribir 2 variables consume menos gas que sobrescribir 1? No puedo imaginar esto posible. ¿Podrías explicar más por favor?
Y aún así, no puedo entender qué quiere decir exactamente con "reembolso de gas". Si tal "reembolso de gas" es posible, ¿qué pasa si hago un método que se beneficiará de él? Como establecer una variable, liberarla y obtener un reembolso, configurarla nuevamente, liberarla nuevamente y así sucesivamente. Parece que la máquina virtual puede atascarse aquí si se le da un bucle de "reembolso de gasolina" tan bien diseñado. ¿O me estoy perdiendo algo? Estoy deseando saber.
Sí, ese es el punto de esta pregunta. :-) Escribir un cero cuesta 5000 de gasolina, y si anteriormente había un cero, obtienes un reembolso de 15000 de gasolina. Los reembolsos de gas se acumulan y pueden compensar hasta la mitad del gas consumido por la transacción. (Pero tenga en cuenta que hay un costo de transacción mínimo: 21000). Entonces, la matemática de un reembolso de gasolina es algo así como gas_consumed = max(21000, gas_consumed - min(gas_consumed / 2, gas_refund)).
Cada iteración de x = 5; x = 0;un bucle costaría 20000 (costo de escribir un valor distinto de cero donde antes había un cero) + 5000 (costo de escribir un cero) en gasolina y agregaría 15000 al reembolso de gasolina. No se obtendría ninguna ganancia haciendo eso.
No importa @smarx, al leer detenidamente este artículo tengo de lo que estabas hablando. ¡Gracias por quedarte aquí!
Nota de errata rápida de @smarx: ¡su fórmula de gas consumido está desactivada ya que el costo del gas puede ser inferior a 21000 con reembolsos!
@ZitRo en aras de la claridad, si el valor de la matriz se asigna en el constructor, entonces deleteya no establecería la posición en 0 necesariamente, sino en el valor inicial.