Skip to content

Commit

Permalink
Add findFirstUnset
Browse files Browse the repository at this point in the history
  • Loading branch information
Vectorized committed Jul 21, 2024
1 parent 753c57b commit ff39fab
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 27 deletions.
36 changes: 19 additions & 17 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -624,34 +624,36 @@ LibBitTest:testReverseBitsDifferential(uint256) (runs: 292, μ: 18724, ~: 18724)
LibBitTest:testReverseBytes() (gas: 12492)
LibBitTest:testReverseBytesDifferential(uint256) (runs: 292, μ: 2675, ~: 2675)
LibBitTest:test__codesize() (gas: 5851)
LibBitmapTest:testBitmapClaimWithGetSet() (gas: 27155)
LibBitmapTest:testBitmapClaimWithToggle() (gas: 17392)
LibBitmapTest:testBitmapClaimWithGetSet() (gas: 27111)
LibBitmapTest:testBitmapClaimWithToggle() (gas: 17479)
LibBitmapTest:testBitmapFindFirstUnset() (gas: 54669)
LibBitmapTest:testBitmapFindFirstUnset(uint256,uint256,uint256) (runs: 292, μ: 143207, ~: 142829)
LibBitmapTest:testBitmapFindLastSet() (gas: 1300541)
LibBitmapTest:testBitmapFindLastSet(uint256,uint256) (runs: 292, μ: 76177, ~: 76187)
LibBitmapTest:testBitmapFindLastSet2() (gas: 23882)
LibBitmapTest:testBitmapFindLastSet(uint256,uint256) (runs: 292, μ: 76221, ~: 76237)
LibBitmapTest:testBitmapFindLastSet2() (gas: 23905)
LibBitmapTest:testBitmapGet() (gas: 2513)
LibBitmapTest:testBitmapGet(uint256) (runs: 292, μ: 2586, ~: 2586)
LibBitmapTest:testBitmapPopCount() (gas: 752476)
LibBitmapTest:testBitmapPopCount(uint256,uint256,uint256) (runs: 292, μ: 215272, ~: 192265)
LibBitmapTest:testBitmapPopCountAcrossMultipleBuckets() (gas: 73611)
LibBitmapTest:testBitmapPopCount() (gas: 752459)
LibBitmapTest:testBitmapPopCount(uint256,uint256,uint256) (runs: 292, μ: 217167, ~: 193549)
LibBitmapTest:testBitmapPopCountAcrossMultipleBuckets() (gas: 73634)
LibBitmapTest:testBitmapPopCountWithinSingleBucket() (gas: 34054)
LibBitmapTest:testBitmapSet() (gas: 22549)
LibBitmapTest:testBitmapSet(uint256) (runs: 292, μ: 22621, ~: 22621)
LibBitmapTest:testBitmapSetAndGet(uint256) (runs: 292, μ: 22655, ~: 22655)
LibBitmapTest:testBitmapSetBatch() (gas: 2918674)
LibBitmapTest:testBitmapSetBatchAcrossMultipleBuckets() (gas: 438393)
LibBitmapTest:testBitmapSetAndGet(uint256) (runs: 292, μ: 22633, ~: 22633)
LibBitmapTest:testBitmapSetBatch() (gas: 2918652)
LibBitmapTest:testBitmapSetBatchAcrossMultipleBuckets() (gas: 438416)
LibBitmapTest:testBitmapSetBatchWithinSingleBucket() (gas: 389011)
LibBitmapTest:testBitmapSetTo() (gas: 14292)
LibBitmapTest:testBitmapSetTo(uint256,bool,uint256) (runs: 292, μ: 12504, ~: 2888)
LibBitmapTest:testBitmapSetTo(uint256,uint256) (runs: 292, μ: 46180, ~: 50296)
LibBitmapTest:testBitmapToggle() (gas: 30828)
LibBitmapTest:testBitmapToggle(uint256,bool) (runs: 292, μ: 18971, ~: 23125)
LibBitmapTest:testBitmapUnset() (gas: 22528)
LibBitmapTest:testBitmapUnset(uint256) (runs: 292, μ: 14322, ~: 14336)
LibBitmapTest:testBitmapUnsetBatch() (gas: 2981241)
LibBitmapTest:testBitmapSetTo(uint256,uint256) (runs: 292, μ: 45214, ~: 50098)
LibBitmapTest:testBitmapToggle() (gas: 30810)
LibBitmapTest:testBitmapToggle(uint256,bool) (runs: 292, μ: 18970, ~: 23125)
LibBitmapTest:testBitmapUnset() (gas: 22572)
LibBitmapTest:testBitmapUnset(uint256) (runs: 292, μ: 14323, ~: 14337)
LibBitmapTest:testBitmapUnsetBatch() (gas: 2981264)
LibBitmapTest:testBitmapUnsetBatchAcrossMultipleBuckets() (gas: 438470)
LibBitmapTest:testBitmapUnsetBatchWithinSingleBucket() (gas: 445869)
LibBitmapTest:test__codesize() (gas: 7253)
LibBitmapTest:test__codesize() (gas: 8512)
LibCWIATest:testCloneDeteministicWithImmutableArgs() (gas: 191687)
LibCWIATest:testCloneDeteministicWithImmutableArgs(address,uint256,uint256[],bytes,uint64,uint8,uint256) (runs: 292, μ: 1099343, ~: 1052369)
LibCWIATest:testCloneWithImmutableArgs() (gas: 120548)
Expand Down
57 changes: 47 additions & 10 deletions src/utils/LibBitmap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -169,31 +169,68 @@ library LibBitmap {
view
returns (uint256 setBitIndex)
{
uint256 bucket;
uint256 bucketBits;
setBitIndex = NOT_FOUND;
uint256 bucket = upTo >> 8;
uint256 bits;
/// @solidity memory-safe-assembly
assembly {
setBitIndex := not(0)
bucket := shr(8, upTo)
mstore(0x00, bucket)
mstore(0x20, bitmap.slot)
let offset := and(0xff, not(upTo)) // `256 - (255 & upTo) - 1`.
bucketBits := shr(offset, shl(offset, sload(keccak256(0x00, 0x40))))
if iszero(or(bucketBits, iszero(bucket))) {
bits := shr(offset, shl(offset, sload(keccak256(0x00, 0x40))))
if iszero(or(bits, iszero(bucket))) {
for {} 1 {} {
bucket := add(bucket, setBitIndex) // `sub(bucket, 1)`.
mstore(0x00, bucket)
bucketBits := sload(keccak256(0x00, 0x40))
if or(bucketBits, iszero(bucket)) { break }
bits := sload(keccak256(0x00, 0x40))
if or(bits, iszero(bucket)) { break }
}
}
}
if (bucketBits != 0) {
setBitIndex = (bucket << 8) | LibBit.fls(bucketBits);
if (bits != 0) {
setBitIndex = (bucket << 8) | LibBit.fls(bits);
/// @solidity memory-safe-assembly
assembly {
setBitIndex := or(setBitIndex, sub(0, gt(setBitIndex, upTo)))
}
}
}

/// @dev Returns the index of the least significant unset bit in `[begin..upTo]`.
/// If no unset bit is found, returns `NOT_FOUND`.
function findFirstUnset(Bitmap storage bitmap, uint256 begin, uint256 upTo)
internal
view
returns (uint256 unsetBitIndex)
{
unsetBitIndex = NOT_FOUND;
uint256 bucket = begin >> 8;
uint256 negBits;
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, bucket)
mstore(0x20, bitmap.slot)
let offset := and(0xff, begin)
negBits := shl(offset, shr(offset, not(sload(keccak256(0x00, 0x40)))))
if iszero(negBits) {
let lastBucket := shr(8, upTo)
for {} 1 {} {
bucket := add(bucket, 1)
mstore(0x00, bucket)
negBits := not(sload(keccak256(0x00, 0x40)))
if or(negBits, gt(bucket, lastBucket)) { break }
}
if gt(bucket, lastBucket) {
negBits := shl(and(0xff, not(upTo)), shr(and(0xff, not(upTo)), negBits))
}
}
}
if (negBits != 0) {
uint256 r = (bucket << 8) | LibBit.ffs(negBits);
/// @solidity memory-safe-assembly
assembly {
unsetBitIndex := or(r, sub(0, or(gt(r, upTo), lt(r, begin))))
}
}
}
}
45 changes: 45 additions & 0 deletions test/LibBitmap.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,51 @@ contract LibBitmapTest is SoladyTest {
}
}

function testBitmapFindFirstUnset() public {
assertEq(bitmap.findFirstUnset(0, 1000), 0);
assertEq(bitmap.findFirstUnset(1, 1000), 1);
assertEq(bitmap.findFirstUnset(255, 1000), 255);
assertEq(bitmap.findFirstUnset(256, 1000), 256);
bitmap.set(0);
assertEq(bitmap.findFirstUnset(0, 1000), 1);
bitmap.map[0] = type(uint256).max;
assertEq(bitmap.findFirstUnset(0, 1000), 256);
bitmap.set(256);
assertEq(bitmap.findFirstUnset(0, 1000), 257);
assertEq(bitmap.findFirstUnset(0, 255), LibBitmap.NOT_FOUND);
assertEq(bitmap.findFirstUnset(0, 256), LibBitmap.NOT_FOUND);
assertEq(bitmap.findFirstUnset(0, 257), 257);
assertEq(bitmap.findFirstUnset(10, 9), LibBitmap.NOT_FOUND);
assertEq(bitmap.findFirstUnset(1000, 9), LibBitmap.NOT_FOUND);
}

function testBitmapFindFirstUnset(uint256 begin, uint256 upTo, uint256) public {
unchecked {
for (uint256 i; i != 5; ++i) {
bitmap.map[i] = type(uint256).max;
}
}

do {
begin = _bound(_random(), 0, 1000);
upTo = _bound(_random(), 0, 1000);
} while (begin > upTo);

uint256 expected = _bound(_random(), 0, 1000);
bitmap.unset(expected);
assertEq(
bitmap.findFirstUnset(begin, upTo),
expected < begin || expected > upTo ? LibBitmap.NOT_FOUND : expected
);

do {
begin = _bound(_random(), 0, 1000);
upTo = _bound(_random(), 0, 1000);
} while (!(begin > upTo));

assertEq(bitmap.findFirstUnset(begin, upTo), LibBitmap.NOT_FOUND);
}

function testBitmapFindLastSet() public {
unchecked {
bitmap.unsetBatch(0, 2000);
Expand Down

0 comments on commit ff39fab

Please sign in to comment.