Являются ли массивы динамических значений более эффективными, чем массивы ссылок в Solidity?

Фон

Во время разработки и тестирования шаблонов смарт-контрактов доступа к данным (S-DAC) Datona Labs Solidity нам часто приходится обрабатывать данные с небольшим, но неизвестным количеством элементов, например идентификаторами пользователей. В идеале они хранятся в небольших динамических массивах небольших значений.

В примерах для этой статьи мы исследуем, помогает ли нам использование динамических массивов значений сделать это более эффективно, чем использование ссылочных массивов или аналогичных решений в Solidity.

Обсуждение

Если у нас есть данные, содержащие известное небольшое количество небольших чисел, мы можем использовать массив значений в Solidity, как указано в статье автора Массивы значений в Solidity, в которой мы предоставляем и измеряем массивы значений Solidity. Мы пришли к выводу, среди прочего, что мы можем уменьшить объем хранилища и потребление газа, используя массивы значений во многих случаях.

Этот вывод был сделан потому, что Solidity работает на виртуальной машине Ethereum (EVM), которая имеет очень большое машинное слово в 256 бит (32 байта). Эта функция в сочетании с высоким потреблением газа для обработки эталонных массивов побуждает нас рассмотреть возможность использования массивов значений.

Однако, если мы предоставляем наши собственные библиотеки для управления массивами фиксированных значений, давайте определим, возможно ли также предоставить массивы динамических значений.

Давайте сравним массивы динамических значений с массивами фиксированных значений и собственными фиксированными и динамическими массивами Solidity.

Мы также сравним структуру, содержащую длину и фиксированный массив, а также структуру, содержащую массив значений.

Возможные массивы динамических значений

В Solidity возможны только массивы динамической памяти. Массивы памяти имеют фиксированный размер и не могут использовать push() для добавления дополнительных элементов.

Поскольку мы предоставляем собственный код для массивов динамических значений в библиотеках Solidity, мы также можем предоставить push() pop()) для использования как в хранилищах, так и в массивах памяти.

Массивы динамических значений должны будут записывать текущую длину массива и управлять ею. В следующем коде мы решили хранить длину в старших битах значения 256-битного, 32-байтового машинного слова.

Массивы динамических значений

Это массивы динамических значений, которые соответствуют некоторым доступным типам Solidity:

Dynamic Value Arrays
Type           Type Name   Description
uint128[](1)   uint128d1   one 128bit element value
uint64[](3)    uint64d3    three 64bit element values
uint32[](7)    uint32d7    seven 32bit element values
uint16[](15)   uint16d15   fifteen 16bit element values
uint8[](31)    uint8d31    thirty-one 8bit element values

Мы предлагаем имя типа, как показано выше, которое используется в этой статье, но вы можете найти предпочтительное соглашение об именах.

Вот организация данных типа массива значений uint8d31:

Ниже мы рассмотрим uint8d31 более подробно.

Более динамические массивы значений

Очевидно, что существует больше возможных массивов значений. Предполагая, что мы резервируем верхние биты 256-битного значения для хранения максимальной длины динамического массива, количество бит в значении X, умноженное на количество элементов Y, должно быть меньше или равно 256 минус достаточно бит для хранения массива. длина, L:

More Dynamic Value Arrays
Type           Type Name  Len  Description
uintX[](Y)     uintXdY     L   X * Y <= 256 - L
uint255[](1)   uint255d1   1   one 248bit element value
uint126[](2)   uint126a2   2   two 124bit element values
uint84[](3)    uint84d3    2   three 82bit element values
uint63[](4)    uint63d4    3   four 62bit element values
uint50[](5)    uint50d5    3   five 51bit element values
uint42[](6)    uint42d6    3   six 42bit element values
uint36[](7)    uint36d7    3   seven 36bit element values
uint31[](8)    uint31d8    4   eight 31bit element values
uint28[](9)    uint28d9    4   nine 28bit element values
uint25[](10)   uint25d10   4   ten 25bit element values
uint22[](11)   uint22d11   4   eleven 22bit element values
uint21[](12)   uint21d12   4   twelve 21bit element values
uint19[](13)   uint19d13   4   thirteen 19bit element values
uint18[](14)   uint18d14   4   fourteen 18bit element values
uint16[](15)   uint16d15   4   as above
uint15[](16)   uint15d16   5   sixteen 15bit element values
uint14[](17)   uint14d17   5   seventeen 14bit element values
uint13[](19)   uint13d19   5   nineteen 13bit element values
uint12[](20)   uint12d20   5   twenty 12bit element values
uint11[](22)   uint11d22   5   twenty-two 11bit element values
uint10[](25)   uint10d25   5   twenty-five 10bit element values
uint9[](27)    uint9d27    5   twenty-seven 9bit element values
uint8[](31)    uint8d31    5   as above
uint7[](35)    uint7d35    6   thirty-five 7bit element values
uint6[](41)    uint6d41    6   forty-one 6bit element values
uint5[](50)    uint5d50    6   fifty 5bit element values
uint4[](62)    uint4d62    6   sixty-two 4bit element values
uint3[](83)    uint3d83    7   eighty-three 3bit element values
uint2[](124)   uint2d124   7   one-hundred & twenty-four 2bit EVs
uint1[](248)   uint1d248   8   two-hundred & forty-eight 1bit EVs

Необходимый тип массива зависит от проекта. Кроме того, может потребоваться несколько типов массивов, например uint8d31 для идентификаторов пользователей и uint5d50 для ролей.

Вот организация данных для выбранных типов массивов значений:

Обратите внимание на массив значений uint1d248. Это позволяет нам эффективно кодировать до 248 однобитовых значений элементов, которые представляют логические значения, в одно слово EVM. Сравните это с bool [248] в Solidity, который потребляет в 248 раз больше места в памяти и даже в восемь раз больше места в хранилище.

Реализация динамического массива значений

Вот полезный файл импорта, обеспечивающий функции получения и установки для типа массива динамических значений uint8d31:

// uint8d31.sol
library uint8d31 { // provides the equivalent of uint8[](31)
    uint constant wordBits = 256;
    uint constant bits = 8;
    uint constant elements = 31;
    uint constant lenBits = 5;
    // ensure that (bits * elements) <= (wordBits - lenBits)
    
    uint constant range = 1 << bits;
    uint constant max = range - 1;
    uint constant lenPos = wordBits - lenBits;
    
    function length(uint va) internal pure returns (uint) {
        return va >> lenPos;
    }
    function setLength(uint va, uint len) internal pure returns
    (uint) {
        require(len <= elements);
        return (va & (uint(~0x0) >> lenBits)) | (len << lenPos);
    }
    function get(uint va, uint index) internal pure returns (uint) {
        require(index < (va >> lenPos));
        return (va >> (bits * index)) & max;
    }
    function set(uint va, uint index, uint value) internal pure 
    returns (uint) {
        require((index < (va >> lenPos)) && (value < range));
        index *= bits;
        return (va & ~(max << index)) | (value << index);
    }
    function push(uint va, uint value) internal pure returns (uint){
        uint len = va >> lenPos;
        require((len < elements) && (value < range));
        uint posBits = len * bits;
        va = (va & ~(max << posBits)) | (value << posBits);
        return (va & (uint(~0) >> lenBits)) | ((len + 1) << lenPos);
    }
}

Функция length() возвращает текущий размер массива динамических значений. Вы можете изменить количество элементов в массиве с помощью setLength() или push() .

Функции get() и set() получают и устанавливают определенный элемент в соответствии с массивами фиксированных значений, за исключением того, что могут быть доступны только элементы, которые находятся в пределах текущего размера массива.

Функция push() добавляет значения до максимального размера массива динамических значений. Просто определите pop() , чтобы обеспечить эффективный стек малых значений.

Давайте посмотрим на несколько простых тестов в солнечный день для примера кода библиотеки uint8d31:

import "uint8d31.sol";
contract TestUint8d31 {
    using uint8d31 for uint;
    
    function test1() public pure {
        uint va;
        require(va.length() == 0, "length not 0");
        va = va.setLength(10);
        require(va.length() == 10, "length not 10");
    }
  
    function test2() public {
        uint va;
        va = va.push(0x12);
        require(va.get(0) == 0x12, "va[0] not 0x12");
        require(va.length() == 1, "length not 1");
        
        va = va.push(0x34);
        require(va.get(1) == 0x34, "va[1] not 0x34");
        require(va.length() == 2, "length not 2");
        
        va = va.setLength(31);
        require(va.length() == 31, "length not 31");
        va = va.set(30, 0x78);
        require(va.get(30) == 0x78, "va[30] not 0x78");
        require(va.length() == 31, "length not 31");
    }
}

Структурировать динамические массивы

Преимущество использования структур заключается в том, что они передаются по ссылке во внутренние (не внешние) библиотечные функции, что исключает необходимость назначать возвращаемое значение функции из setLength(), set() и push().

Вот структура, содержащая 31 байт данных в фиксированном массиве и длине, и связанные с ней библиотечные функции:

struct Suint8u31 { // struct representing uint8[](31)
    uint8[31] data;
    uint8 length;
}
// ------------
library Suint8u31lib {
    // constant declarations as per uint8d31
    
    function length(Suint8d31 memory s) internal pure returns (uint)
    {
        return s.length;
    }
    function setLength(Suint8d31 memory s, uint len) ...
    // other function definitions similar to uint8d31
}

Этот код похож на uint8d31, просто заменяя s.length и s.data[index] там, где это необходимо, и не возвращая значение из setLength(), set() или push().

Определенная выше структура Suint8u31, по-видимому, занимает 256 бит адресного пространства. Но в Solidity каждый массив содержит дополнительное 256-битное значение длины массива, даже если это фиксированный массив, поэтому мы ожидаем, что потребление газа в этом решении будет выше, чем предполагалось.

Структурировать массивы динамических значений

Вот структура, содержащая массив динамических значений и связанные библиотечные функции:

struct Suint8d31 { // struct representing uint8[](31)
    uint va; // uint8d31 value array
}
// ------------
library Suint8d31lib {
    // as per uint8d31
    
    function length(Suint8d31 memory s) internal pure returns (uint)
    {
        return s.va >> lenPos;
    }
    function setLength(Suint8u31 memory s, uint len) ...
    // other function definitions similar to uint8d31
}

Этот код очень похож на uint8d31, просто заменяя s.va для каждого вхождения va и не возвращая значение из setLength(), set() или push().

Давайте измерим расход газа сразу после этого загадочного перерыва на комфорт.

Расход газа

Написав библиотеки и контракты, мы измерили расход газа по методике, описанной в статье автора Функции твердости газа.

Вот условные обозначения диаграмм, представленных ниже:

Legend         Meaning
uint8[](32)    Solidity dynamic array of 32 uint8
uint8[32]      Solidity fixed array of 32 uint8
uint8a32       Fixed Value Array of 32 uint8 (other article)
uint8d31       Dynamic Value Array of <= 31 uint8 (this article)
suint8d31      Struct containing Dynamic Value Array of <= 31 uint8
suint8u31      Struct containing Solidity fixed array of <= 31 uint8

uint8 массивы в пространстве памяти EVM

Здесь мы сравниваем использование динамических массивов uint8 в пространстве памяти EVM:

Эта диаграмма показывает, что потребление газа для нескольких общих операций с массивом динамических значений (uint8d31) потребляет лишь немного больше газа, чем массив фиксированных значений (uint8a32).

Все остальные параметры потребляют значительно больше газа, особенно структура, содержащая фиксированный массив Solidity (последний столбец).

Вот как сравниваются расходы газа на отдельные операции:

Обратите внимание, что push() не разрешен для массивов памяти Solidity, даже динамических (золотой столбец для каждого типа), но мы реализовали его для динамических структур данных, измеренных в этой статье.

Вывод из этого заключается в том, что, опять же, массив динамических значений (uint8d31) потребляет лишь немного больше газа, чем массив фиксированных значений (uint8a32), а все другие параметры потребляют (иногда намного) больше газа.

массивы uint8 в хранилище EVM

Здесь мы сравниваем использование динамических массивов uint8 в пространстве хранения EVM:

Здесь, не считая большого расхода газа в первом и последнем столбцах, картина ясна, а выбор менее однозначен.

Вот как сравниваются расходы газа на отдельные операции:

Столбец, на котором следует сосредоточиться, вероятно, является столбцом ржавчины (крайний правый для каждого типа), который имеет тенденцию показывать типичное использование после выделения места для хранения, тогда как первый push() или set() вызывает выделение пространства для хранения, что потребляет много газа на ЭВМ.

Выше массив динамических значений (uint8d31) потребляет немного больше газа, чем массив фиксированных значений (uint8a32), а все остальные параметры потребляют немного больше газа.

Параметры субподрядов и библиотек

Неудивительно, что наибольшее потребление газа связано с предоставлением параметра массива субподрядной или библиотечной функции, а затем повторным получением значения.

Очевидно, что использование значения потребляет гораздо меньше газа.

Другие возможности

Если вы сочтете полезными динамические массивы значений, вы также можете рассмотреть массивы фиксированных значений, фиксированные многозначные массивы, очереди значений, стеки значений и т. Д. И как бы работали ваши алгоритмы (такие как Sort), если бы они использовали массивы значений вместо ссылочных массивов?

Выводы

Мы предоставили и измерили код для универсального библиотечного кода для небольших массивов динамических значений uintX [] (Y).

Мы можем уменьшить пространство для хранения и потребление газа, используя массивы динамических значений по сравнению с динамическими массивами Solidity.

Если в ваших смарт-контрактах Solidity используются небольшие динамические массивы небольших значений (для идентификаторов пользователей, ролей и т. Д.), То использование массивов динамических значений, вероятно, потребует меньше газа.

Если массивы копируются, например, для субподряда или библиотек, массивы динамических значений всегда потребляют значительно меньше газа.

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

Спасибо за прочтение!