3

I have a simple contract that deletes the last element of the array:

pragma solidity^0.4.11;

contract GasRefundTest {

    uint[] myArray = [1, 2];

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

Transaction cost for calling deleteLastElem() is 17182 gas.

When I change it to:

pragma solidity^0.4.11;

contract GasRefundTest {

    uint[] myArray = [1, 2];

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

the transaction cost becomes 22480 gas.

I thought deleting storage slots should result in gas refund, instead I see gas increase.

Can anyone explain what's going on here.

medvedev1088
  • 10,996
  • 5
  • 35
  • 63

2 Answers2

3

Reducing the size of a dynamic-size array already zeroes out the elements that were "removed."

So the version of your code that first does a delete myArray[1] is just doing an extra write to storage that's about to be done anyway.

Fun things to try:

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

EDIT

I should point out that this last example is a bit confusing. The gas refund does kick in there, but it's limited to half the consumed gas.

user19510
  • 27,999
  • 2
  • 30
  • 48
  • Thank you for the answer. Please explain about why 17k gas is impossible. I saw in the comments you mention that gas paid should be at least 21k however I didn't find this mentioned anywhere in the yellowpaper or sources. From what I understand without the refund the transaction would cost 21k + 5k + 5k =~31k (intrinsic gas + setting length + setting last elem to 0). Half of that is 15.5k. After refunding this amount consumed gas becomes 15500. Round about what remix shows. – medvedev1088 Feb 12 '18 at 07:33
  • In my previous comment I meant 15.5k cap which is greater than 15k so the 15k will be refunded, totalling to 16k. – medvedev1088 Feb 12 '18 at 07:40
  • 1
    @medvedev1088 I'm looking for a source, but I can't find it in the Yellow Paper either. Perhaps I'm mistaken. – user19510 Feb 12 '18 at 07:45
  • 1
    @medvedev1088 You are correct. The only limit on refunds is half the consumed gas, so consuming less than 21000 gas is possible. (I observed actual transactions on mainnet with less than 21000 gas consumed.) – user19510 Feb 12 '18 at 07:49
  • Btw the fact that decreasing array length is O(n) time instead of expected constant time, and increasing array length is constant time instead of expected O(n) is counter intuitive and should be mentioned in the documentation. – medvedev1088 Feb 12 '18 at 08:22
  • It should be O(n) time, right? Not O(log(n)). – user19510 Feb 12 '18 at 08:23
  • Ah right, typo :) – medvedev1088 Feb 12 '18 at 08:25
  • 1
    Also, I agree that this should be mentioned in the documentation. I had to experiment to find out which operation did the zeroing. (I assumed one would have to.) Given that all storage is implicitly initialized to zero, though, it makes sense that it's shortening the array that has to do the work. (Most of the time, it would be a waste to zero storage during array expansion.) – user19510 Feb 12 '18 at 08:25
  • Also I feel that resetting unoccupied slots shouldn't be a compiler's job. It'd better be implemented as a library code (likely in assembly). In many cases people want to control whether and how to zero out slots (e.g. if it's already 0 don't touch it). – medvedev1088 Feb 12 '18 at 08:41
  • I would argue that it's a good default behavior for the storage to be zeroed out when shrinking an array. (Anything else would be quite surprising for the developer.) You could always skip decreasing myArray.length and instead keep your own variable indicating how many elements are valid. Then you could implement whatever logic you want around when to zero things out. – user19510 Feb 12 '18 at 08:45
  • I agree that it should be the default behavior. Moving it from the compiler to a library would be more transparent and flexible. – medvedev1088 Feb 12 '18 at 08:55
  • On the other hand the array is a built in data structure so it makes sense they implemented most operations in the compiler. If necessary a wrapper can be implemented as a library that would have the logic that you described above. – medvedev1088 Feb 12 '18 at 09:01
  • Or instead of putting this logic into length property they could add resize function for arrays. This way it's more clear that this is not just setting the property but also all the work associated with resizing the array. Many people don't expect that setting length will do some heavy work. In this top answer for example the author didn't expect it also so cleared up slot himself https://ethereum.stackexchange.com/a/1528/18932 – medvedev1088 Feb 12 '18 at 09:15
1

To explain at the high level, you are doing 2 operations instead of 1, hence it requires more gas. All the code you write is compiled to low-level Ethereum Virtual Machine (EVM) commands, which then are interpreted by it. For each such command there is a particular gas price defined, look at this.

Now, in the second case, you use delete. From the docs,

delete a assigns the initial value for the type to a

It is important to note that delete a really behaves like an assignment to a, i.e. it stores a new object in a.

So you are putting 0 there first, before decreasing the length of an array. Purely for the sake of interest, you may try to use myArray[1] = 0; instead and see how this affects the gas used.

nikitaeverywhere
  • 307
  • 1
  • 2
  • 9
  • From the original question: "I thought deleting storage slots should result in gas refund, instead I see gas increase." See my answer for why this isn't the case here. – user19510 Feb 11 '18 at 21:48
  • @smarx, I saw your answer and basically we are talking about the same. – nikitaeverywhere Feb 11 '18 at 21:49
  • I don't think the confusion was about why doing two things costs more than one. It was why the gas refund isn't reducing the gas price. – user19510 Feb 11 '18 at 21:52
  • @smarx, it was, and my answer clarifies that as well as yours, isn't it? – nikitaeverywhere Feb 11 '18 at 21:53
  • I'm completely confused now. I'm not sure if I'm misunderstanding your answer or you're misunderstanding mine. – user19510 Feb 11 '18 at 22:00
  • @smarx, my answer without the first paragraph reads the same as yours. The first paragraph just explains how EVM works in general for the reader to understand why assignment takes gas instead of refunding it. Maybe, I should have structured my answer in a different way to resolve the confusion. – nikitaeverywhere Feb 11 '18 at 22:09
  • The difference I see is that your answer doesn't address why writing a zero fails to decrease gas usage as you would typically expect. (foo = 6; foo = 0; is "2 operations instead of 1" but consumes less gas than just foo = 6;) – user19510 Feb 11 '18 at 22:20
  • @smarx but I don’t see your answer to address this either ;) Regarding your example, it would be nice to hear from you why — compiler optimization? Compiler optimization + no actual write? (In case of foo was equal to 0 before assignment) – nikitaeverywhere Feb 11 '18 at 22:26
  • It's the first line of my answer: "Reducing the size of a dynamic-size array already zeroes out the elements that were 'removed.'" Restated, myArray.length-- has a side effect of first doing delete myArray[myArray.length - 1]; So the gas refund is already achieved, and the extra delete does not gain you anything. – user19510 Feb 11 '18 at 22:28
  • Sorry, I was assuming in my foo = 6; foo = 0; example that foo was not already 0. – user19510 Feb 11 '18 at 22:28
  • And I haven't tested it with compiler optimizations enabled. Hopefully the compiler would indeed not bother with the foo = 6. My point was just that "2 operations are more expensive than 1" is not accurate due to gas refunds. Perhaps a clearer example would be foo = 6; bar = 0; consuming less gas than just foo = 6 (again, where bar was previously non-zero). – user19510 Feb 11 '18 at 22:30
  • Wait, did you say overwriting 2 variables consumes less gas than overwriting 1? I cannot imagine this possible. Could you explain more please? – nikitaeverywhere Feb 11 '18 at 22:33
  • And still, I cannot get what do you mean exactly under “gas refund”. If such “gas refund” is possible, what if I do a method which will benefit from it? Like set variable — free it and get a refund — set it again — free it again and so on. Sounds like virtual machine can stuck here if given such well-designed “gas refund” loop. Or, am I missing something? Looking forward to know. – nikitaeverywhere Feb 11 '18 at 22:39
  • Yes, that's the whole point of this question. :-) Writing a zero costs 5000 gas, and if there was previously a non-zero there, you get a 15000 gas refund. Gas refunds are accumulated, and they can offset up to half of the gas consumed by the transaction. (But note that there is a minimum transaction cost: 21000.) So the math of a gas refund is something like gas_consumed = max(21000, gas_consumed - min(gas_consumed / 2, gas_refund)). – user19510 Feb 11 '18 at 22:40
  • Each iteration of x = 5; x = 0; in a loop would cost 20000 (cost of writing a non-zero where there was previously a zero) + 5000 (cost of writing a zero) in gas and would add 15000 to the gas refund. There would be no gain to be had by doing that. – user19510 Feb 11 '18 at 22:43
  • Never mind @smarx, by reading carefully through this article I’ve got what you were talking about. Thanks for staying here! – nikitaeverywhere Feb 11 '18 at 22:58
  • @smarx quick errata note, your gas consumed formula is off since gas cost can go below 21000 with refunds! – Garen Vartanian Oct 16 '18 at 18:33
  • @ZitRo for the sake of clarity, if the array value is assigned in the constructor, then delete would no longer set the position to 0 necessarily, but to whatever the initial value was. – Garen Vartanian Oct 16 '18 at 18:37