Сброс слота для хранения увеличивает использование газа, хотя должен уменьшить его

У меня есть простой контракт, который удаляет последний элемент массива:

pragma solidity^0.4.11;

contract GasRefundTest {

    uint[] myArray = [1, 2];

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

Стоимость транзакции для вызова deleteLastElem()составляет 17182 газа.

Когда я меняю его на:

pragma solidity^0.4.11;

contract GasRefundTest {

    uint[] myArray = [1, 2];

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

стоимость сделки становится 22480 газа.

Я думал, что удаление слотов для хранения должно привести к возврату газа, вместо этого я вижу увеличение количества газа.

Кто-нибудь может объяснить, что здесь происходит.

Похоже, лучший способ ответить на этот вопрос — перейти на более низкий уровень и подробно описать оба процесса (байт-код или коды операций).
Я согласен. Изучение байт-кода или сборки поможет прояснить ситуацию.

Ответы (2)

Уменьшение размера массива динамического размера уже обнуляет элементы, которые были «удалены».

Таким образом, версия вашего кода, которая сначала выполняет a delete myArray[1], просто выполняет дополнительную запись в хранилище, которая в любом случае будет выполнена.

Интересные вещи, которые стоит попробовать:

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

РЕДАКТИРОВАТЬ

Я должен отметить, что этот последний пример немного сбивает с толку. Компенсация за газ есть , но она ограничена половиной потребленного газа.

Спасибо за ответ. Объясните пожалуйста почему 17к газ невозможен. Я видел в комментариях, что вы упомянули, что оплата за газ должна быть не менее 21 тысячи, однако я не нашел этого нигде в желтой бумаге или источниках. Насколько я понимаю, без возмещения транзакция будет стоить 21k + 5k + 5k = ~ 31k (внутренний газ + длина настройки + установка последнего элемента на 0). Половина этой суммы составляет 15,5 тыс. После возмещения этой суммы израсходованного газа становится 15500. Примерно то, что показывает ремикс.
В моем предыдущем комментарии я имел в виду ограничение в 15,5 тысяч, что больше 15 тысяч, поэтому 15 тысяч будут возвращены, что в сумме составит 16 тысяч.
@medvedev1088 Я ищу источник, но не могу найти его и в Yellow Paper. Возможно, я ошибаюсь.
@medvedev1088 Вы правы. Единственным ограничением на возмещение является половина израсходованного газа, поэтому возможно потребление менее 21000 газа. (Я наблюдал реальные транзакции в основной сети с потреблением менее 21000 газа.)
Между прочим, тот факт, что уменьшение длины массива — это время O(n) вместо ожидаемого постоянного времени, а увеличение длины массива — это постоянное время вместо ожидаемого O(n), является нелогичным и должно быть упомянуто в документации.
Должно быть O(n) раз, верно? Не O(log(n)).
Ах да, опечатка :)
Кроме того, я согласен, что это должно быть упомянуто в документации. Пришлось экспериментировать, чтобы выяснить, какая операция выполняет обнуление. (Я полагал, что это необходимо.) Однако, учитывая, что все хранилища неявно инициализируются нулем, имеет смысл укорачивать массив, который должен выполнять работу. (В большинстве случаев было бы напрасно обнулять хранилище во время расширения массива.)
Также я считаю, что сброс незанятых слотов не должен быть задачей компилятора. Его лучше реализовать в виде библиотечного кода (вероятно, на ассемблере). Во многих случаях люди хотят контролировать, как обнулить слоты (например, если он уже равен 0, не трогайте его).
Я бы сказал, что это хорошее поведение по умолчанию для обнуления хранилища при сжатии массива. (Все остальное было бы довольно неожиданно для разработчика.) Вы всегда можете пропустить уменьшение myArray.lengthи вместо этого оставить свою собственную переменную, указывающую, сколько элементов допустимо. Затем вы можете реализовать любую логику, которую хотите, когда обнуляете вещи.
Я согласен, что это должно быть поведением по умолчанию. Перемещение его из компилятора в библиотеку было бы более прозрачным и гибким.
С другой стороны, массив представляет собой встроенную структуру данных, поэтому имеет смысл реализовать большинство операций в компиляторе. При необходимости обертку можно реализовать в виде библиотеки с описанной выше логикой.
Или вместо того, чтобы помещать эту логику в свойство длины, они могли бы добавить функцию изменения размера для массивов. Таким образом, более ясно, что это не просто установка свойства, но и вся работа, связанная с изменением размера массива. Многие люди не ожидают, что установка длины сделает какую-то тяжелую работу. Например, в этом топовом ответе автор этого не ожидал, поэтому сам расчистил слот ethereum.stackexchange.com/a/1528/18932

Чтобы объяснить на высоком уровне, вы выполняете 2 операции вместо 1, поэтому требуется больше газа. Весь код, который вы пишете, компилируется в низкоуровневые команды виртуальной машины Ethereum (EVM), которые затем интерпретируются ею. Для каждой такой команды определена определенная цена газа, посмотрите на это .

Теперь, во втором случае, вы используете delete. Из документов ,

delete a присваивает начальное значение типа a

Важно отметить, что удаление a действительно ведет себя как присваивание a, т. е. оно сохраняет новый объект в a.

Таким образом, вы ставите его 0первым, прежде чем уменьшать длину массива. Чисто ради интереса можно попробовать использовать myArray[1] = 0;вместо него и посмотреть, как это повлияет на используемый газ.

Из первоначального вопроса: «Я думал, что удаление слотов для хранения должно привести к возврату газа, вместо этого я вижу увеличение газа». Смотрите мой ответ, почему это не так здесь.
@smarx, я видел твой ответ, и в основном мы говорим об одном и том же.
Я не думаю, что путаница была в том, почему выполнение двух дел стоит больше, чем одно. Вот почему возврат газа не снижает цену на газ.
@smarx, так и было, и мой ответ проясняет это так же, как и твой, не так ли?
Я сейчас в полном замешательстве. Я не уверен, что я неправильно понимаю ваш ответ, или вы неправильно понимаете мой.
@smarx, мой ответ без первого абзаца читается так же, как и твой. Первый абзац просто объясняет, как работает EVM в целом, чтобы читатель понял, почему назначение требует газа, а не возмещает его. Возможно, мне следовало по-другому структурировать свой ответ, чтобы устранить путаницу.
Разница, которую я вижу, заключается в том, что ваш ответ не объясняет, почему запись нуля не снижает потребление газа, как вы обычно ожидаете. ( foo = 6; foo = 0;это «2 операции вместо 1», но потребляет меньше газа, чем просто foo = 6;)
@smarx, но я также не вижу вашего ответа на этот вопрос;) Что касается вашего примера, было бы неплохо услышать от вас, почему — оптимизация компилятора? Оптимизация компилятора + отсутствие фактической записи? (В случае, если foo был равен 0 до присваивания)
Это первая строка моего ответа: «Уменьшение размера массива динамического размера уже обнуляет элементы, которые были« удалены »». Переформулированный, имеет myArray.length--побочный эффект первого выполнения . ничего вам не даст. delete myArray[myArray.length - 1];delete
foo = 6; foo = 0;Извините, в своем примере я предполагал , что fooэто еще не0 так .
И я не тестировал его с включенной оптимизацией компилятора. Будем надеяться, что компилятор действительно не будет возиться с расширением foo = 6. Моя точка зрения заключалась в том, что «2 операции дороже, чем 1» не соответствует действительности из-за возмещения расходов на газ. Возможно, более наглядным примером будет foo = 6; bar = 0;потребление меньшего количества газа, чем просто foo = 6(опять же, где barраньше было ненулевое значение).
Подождите, вы сказали, что перезапись 2 переменных потребляет меньше газа, чем перезапись 1? Я не могу представить это возможным. Не могли бы вы объяснить больше, пожалуйста?
И все же я не могу понять, что именно вы имеете в виду под «возвратом газа». Если такой «возврат газа» возможен, что, если я сделаю метод, который принесет пользу? Например, установить переменную — освободить ее и получить возмещение — установить снова — снова освободить и так далее. Похоже, виртуальная машина может застрять здесь, если у нее есть такой хорошо продуманный цикл «возврата газа». Или я что-то упускаю? С нетерпением жду знать.
Да, в этом весь смысл этого вопроса. :-) Запись нуля стоит 5000 газа, а если ранее там был не ноль, то вы получаете возврат 15000 газа. Возвраты газа накапливаются, и они могут компенсировать до половины газа, потребленного транзакцией. (Но обратите внимание, что существует минимальная стоимость транзакции: 21000.) Таким образом, математика возмещения расходов на газ выглядит примерно так gas_consumed = max(21000, gas_consumed - min(gas_consumed / 2, gas_refund)).
Каждая итерация x = 5; x = 0;в цикле будет стоить 20 000 (стоимость записи ненулевого значения там, где ранее был ноль) + 5000 (стоимость записи нуля) газа и добавит 15 000 к возврату газа. При этом не было бы никакой выгоды.
Не обращайте внимания на @smarx, внимательно прочитав эту статью , я понял, о чем вы говорили. Спасибо, что остаетесь здесь!
@smarx краткое примечание об ошибках, ваша формула потребления газа отключена, поскольку стоимость газа может упасть ниже 21000 с возмещением!
@ZitRo для ясности, если значение массива присваивается в конструкторе, то deleteбольше не будет обязательно устанавливать позицию в 0, а в любое начальное значение.