diff --git a/.gas-snapshot b/.gas-snapshot index dc116b2437..2597868e91 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -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) diff --git a/src/utils/LibBitmap.sol b/src/utils/LibBitmap.sol index 03859f06c4..c4094c5393 100644 --- a/src/utils/LibBitmap.sol +++ b/src/utils/LibBitmap.sol @@ -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)))) + } + } + } } diff --git a/test/LibBitmap.t.sol b/test/LibBitmap.t.sol index 98efaf3bde..a09a814a69 100644 --- a/test/LibBitmap.t.sol +++ b/test/LibBitmap.t.sol @@ -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);