bit演算関係について、ライブラリとしてではなく、コード書く時の参考をイメージしてまとめました。
https://github.com/eggdragons/how-to-use-foundry
Bit演算関係について
先ほどのリポジトリの中のsrc/bit/Bitwise.sol
には、bit演算、bitシフトについて記載しています。
test/bit/Bitwise.t.sol
が上記のテストになっているのですが、ここでは、bit演算についてコメント記載しています。
例えば、
function and(uint256 x, uint256 y) external pure returns (uint256) {
return x & y;
}
このbit演算の解説??として
function testBitwiseAnd() public {
uint256 x = 10;
uint256 y = 13;
uint256 result = 8;
// x = 10 = 1010
// y = 13 = 1101
// x & y = 1000 = 8
assertEq(bitwise.and(x, y), result);
}
このような形で記載しています。テストコードを見てもらえば、雰囲気掴めるのと、自分でテストしやすいように構成しています。
Bit演算をInline Assembly化
src/bit/Bitwise.sol
のコードをInline Assembly化したものが、src/bit/BitFlagAssembly.sol
になります。
test/bit/BitFlagAssembly.t.sol
では、Inline Assemblyする前とした後の比較テストになっています。
また、Fuzzテストを行うことで、簡易的にArithmetic over/underflow
のチェックを行っています。
Inline Assemblyを使う際、over/underflow
のチェックは重要になってくるので、要チェックですね〜。
例えばですが
function getLastNBits(uint256 x, uint256 n) external pure returns (uint256 result) {
assembly {
let mask := sub(shl(n, 1), 1)
result := and(x, mask)
}
}
このコードは、このまま使えません。nが256以上で必ずオーバーフローします。
もちろんfoundryのfuzzテストでは、エラーが出ます。そこで、下記のようなテストコードになるわけです。
function testBitwiseAssemblyGetLastNBitsFuzz(uint256 x, uint256 n) public {
vm.assume(n < 256);
assertEq(bitwise.getLastNBits(x, n), bitwiseAssembly.getLastNBits(x, n));
}
foundry超便利ですね!
なお、実際に実装する際は、uint256 n
ではなしに、uint8 n
を使うとか、別途処理をかますとかすると安心です。
※一方でケアすればするほど、ガス代が増えるので、悩ましいところですね!
BitFlag関係について
先ほどのリポジトリの中のsrc/bit/BitFlag.sol
には、bitフラッグとbitカウントについて記載しています。
test/bit/BitFlag.t.sol
が上記のテストになっています。
たとえば、N番目に1をセットしたい場合のコードはこちら
function setNBitFlag(uint256 n) public {
uint256 mask = 1 << n;
uint256 clear_mask = ~mask;
bitFlag = (bitFlag & clear_mask) | mask;
}
こちらに対するテストが、こんな感じです。
function testSetNBitFlag() public {
uint256 n = 1;
bitFlag.setNBitFlag(n);
assertEq(bitFlag.checkNBitFlag(n), 1);
// 33 = 100001
bitFlag.setBitFlag(33);
assertEq(bitFlag.checkNBitFlag(2), 0);
assertEq(bitFlag.checkNBitFlag(3), 0);
assertEq(bitFlag.checkNBitFlag(4), 0);
n = 3;
// 100001 --> 101001 = 41
bitFlag.setNBitFlag(n);
assertEq(bitFlag.checkNBitFlag(2), 0);
assertEq(bitFlag.checkNBitFlag(3), 1);
assertEq(bitFlag.checkNBitFlag(4), 0);
assertEq(bitFlag.bitFlag(), 41);
}
色々試せるようになっているので、試してなんとなく掴んでみてください。
BitフラッグをInline Assembly化
Bit演算と同じようにInline Assembly化しています。
そういえば、ガス効率は、foundryのガスレポートで一括出力できるので、ぜひチェックしてみてください。
--gas-report
全く変わらないものもあれば、大幅に変わるものもあります。
Inline Assemblyで内部関数の呼び出し
Inline Assemblyで内部関数の呼び出しを行っています。
function getFirstNBitsCallAssembly(uint256 x, uint256 n) external returns (uint256 result) {
address addr = address(this);
bytes4 sign = bytes4(keccak256("getBitLength(uint256)"));
assembly {
let freePointer := mload(0x40)
mstore(freePointer, sign)
// Set arg
mstore(add(freePointer, 0x04), x)
// Attention GasLimits
let success := call(100000, addr, 0, freePointer, 0x24, freePointer, 0x20)
let len := mload(freePointer)
result := shr(sub(len, n), x)
}
}
コードが見にくくなるので、inline assemblyの内部関数の呼び出しは、おまけ程度に記載しました。
ビット演算一つにとっても、いろんな書き方ができるなぁって思いながら、適当にコードまとめてみました。