Skip to content

Commit 6e95af9

Browse files
feat: massive improvement in gas usage of `BitmapUtils.bitmapToBytesA… (#149)
* feat: massive improvement in gas usage of `BitmapUtils.bitmapToBytesArray` This function was naively using the built-in `bytes.concat` method, but it copies the array to memory, incurring memory expansion costs! Fuzzed tests (e.g. testFuzz_BytesArrayToBitmapToBytesArray) -- which do the encoding twice -- show a ~20k gas improvement with this commit see for reference: https://github.com/Layr-Labs/eigenlayer-middleware/actions/runs/7574740337/job/20629810269#step:5:151-152 * chore: add some additional testing which logs the cost of the `bitmapToBytesArray` function
1 parent 32bd597 commit 6e95af9

File tree

2 files changed

+45
-4
lines changed

2 files changed

+45
-4
lines changed

src/libraries/BitmapUtils.sol

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,27 @@ library BitmapUtils {
115115
* @return bytesArray The resulting bitmap array of bytes.
116116
* @dev Each byte in the input is processed as indicating a single bit to flip in the bitmap
117117
*/
118-
function bitmapToBytesArray(uint256 bitmap) internal pure returns (bytes memory bytesArray) {
118+
function bitmapToBytesArray(uint256 bitmap) internal pure returns (bytes memory /*bytesArray*/) {
119119
// initialize an empty uint256 to be used as a bitmask inside the loop
120120
uint256 bitMask;
121-
// loop through each index in the bitmap to construct the array
122-
for (uint256 i = 0; i < 256; ++i) {
121+
// allocate only the needed amount of memory
122+
bytes memory bytesArray = new bytes(countNumOnes(bitmap));
123+
// track the array index to assign to
124+
uint256 arrayIndex = 0;
125+
/**
126+
* loop through each index in the bitmap to construct the array,
127+
* but short-circuit the loop if we reach the number of ones and thus are done
128+
* assigning to memory
129+
*/
130+
for (uint256 i = 0; (arrayIndex < bytesArray.length) && (i < 256); ++i) {
123131
// construct a single-bit mask for the i-th bit
124132
bitMask = uint256(1 << i);
125133
// check if the i-th bit is flipped in the bitmap
126134
if (bitmap & bitMask != 0) {
127135
// if the i-th bit is flipped, then add a byte encoding the value 'i' to the `bytesArray`
128-
bytesArray = bytes.concat(bytesArray, bytes1(uint8(i)));
136+
bytesArray[arrayIndex] = bytes1(uint8(i));
137+
// increment the bytesArray slot since we've assigned one more byte of memory
138+
unchecked{ ++arrayIndex; }
129139
}
130140
}
131141
return bytesArray;

test/unit/BitmapUtils.t.sol

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,37 @@ contract BitmapUtilsUnitTests_bytesArrayToBitmap is BitmapUtilsUnitTests {
228228
assertEq(bitmap, 8160);
229229
emit log_named_uint("gasSpent", gasSpent);
230230
}
231+
232+
function testFuzz_bitmapToBytesArrayToBitmap(uint256 originalBitmap) public {
233+
uint256 gasLeftBefore = gasleft();
234+
bytes memory bytesArray = bitmapUtilsWrapper.bitmapToBytesArray(originalBitmap);
235+
uint256 gasLeftAfter = gasleft();
236+
uint256 gasSpent = gasLeftBefore - gasLeftAfter;
237+
emit log_named_uint("gas spent by bitmapToBytesArray operation", gasSpent);
238+
uint256 bitmapOutput = bitmapUtilsWrapper.orderedBytesArrayToBitmap(bytesArray);
239+
assertEq(originalBitmap, bitmapOutput, "bitmap output does not match original bitmap!");
240+
}
241+
242+
function test_bitmapToBytesArrayToBitmap_maxBitmap() public {
243+
uint256 originalBitmap = type(uint256).max;
244+
testFuzz_bitmapToBytesArrayToBitmap(originalBitmap);
245+
}
246+
247+
function test_bitmapToBytesArrayToBitmap_emptyBitmap() public {
248+
uint256 originalBitmap = 0;
249+
testFuzz_bitmapToBytesArrayToBitmap(originalBitmap);
250+
}
251+
252+
function test_bitmapToBytesArrayToBitmap_firstTenEntriesBitmap() public {
253+
uint256 originalBitmap = 2047;
254+
testFuzz_bitmapToBytesArrayToBitmap(originalBitmap);
255+
}
256+
257+
function test_bitmapToBytesArrayToBitmap_distributedTenEntriesBitmap() public {
258+
// 2^0+2^10+2^20+2^30+2^40+2^50+2^60+2^70+2^80+2^90
259+
uint256 originalBitmap = 1239150146850664126585242625;
260+
testFuzz_bitmapToBytesArrayToBitmap(originalBitmap);
261+
}
231262
}
232263

233264
contract BitmapUtilsUnitTests_isArrayStrictlyAscendingOrdered is BitmapUtilsUnitTests {

0 commit comments

Comments
 (0)