From c963b46c7eaceebc652930936643365b8c48a021 Mon Sep 17 00:00:00 2001 From: dk1a Date: Fri, 28 Jul 2023 17:28:24 +0300 Subject: [PATCH] refactor(store): optimize Storage library (#1194) --- .changeset/brave-rings-tickle.md | 5 + packages/store/gas-report.json | 236 +++++++++++++---------- packages/store/src/Storage.sol | 201 ++++++++++--------- packages/store/src/StoreCore.sol | 4 +- packages/store/test/Gas.t.sol | 38 +--- packages/store/test/GasStorageLoad.t.sol | 99 ++++++++++ packages/store/test/Storage.t.sol | 4 +- packages/world/gas-report.json | 104 +++++----- 8 files changed, 398 insertions(+), 293 deletions(-) create mode 100644 .changeset/brave-rings-tickle.md create mode 100644 packages/store/test/GasStorageLoad.t.sol diff --git a/.changeset/brave-rings-tickle.md b/.changeset/brave-rings-tickle.md new file mode 100644 index 0000000000..bd9baaf30d --- /dev/null +++ b/.changeset/brave-rings-tickle.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/store": patch +--- + +Optimize storage library diff --git a/packages/store/gas-report.json b/packages/store/gas-report.json index 1835e25afd..4f8c9d2578 100644 --- a/packages/store/gas-report.json +++ b/packages/store/gas-report.json @@ -87,121 +87,157 @@ "file": "test/Gas.t.sol", "test": "testCompareAbiEncodeVsCustom", "name": "pass abi encoded bytes to external contract", - "gasUsed": 6563 + "gasUsed": 6551 }, { "file": "test/Gas.t.sol", "test": "testCompareAbiEncodeVsCustom", "name": "pass custom encoded bytes to external contract", - "gasUsed": 1457 + "gasUsed": 1445 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageMUD", + "test": "testCompareStorageWriteMUD", "name": "MUD storage write (cold, 1 word)", - "gasUsed": 22469 + "gasUsed": 22339 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageMUD", + "test": "testCompareStorageWriteMUD", "name": "MUD storage write (cold, 1 word, partial)", - "gasUsed": 22432 + "gasUsed": 22406 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageMUD", + "test": "testCompareStorageWriteMUD", "name": "MUD storage write (cold, 10 words)", - "gasUsed": 200429 + "gasUsed": 199795 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageMUD", + "test": "testCompareStorageWriteMUD", "name": "MUD storage write (warm, 1 word)", - "gasUsed": 469 + "gasUsed": 339 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageMUD", + "test": "testCompareStorageWriteMUD", "name": "MUD storage write (warm, 1 word, partial)", - "gasUsed": 532 + "gasUsed": 506 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageMUD", + "test": "testCompareStorageWriteMUD", "name": "MUD storage write (warm, 10 words)", - "gasUsed": 2429 + "gasUsed": 1795 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageMUD", - "name": "MUD storage load (warm, 1 word)", - "gasUsed": 630 - }, - { - "file": "test/Gas.t.sol", - "test": "testCompareStorageMUD", - "name": "MUD storage load (warm, 1 word, partial)", - "gasUsed": 577 - }, - { - "file": "test/Gas.t.sol", - "test": "testCompareStorageMUD", - "name": "MUD storage load (warm, 10 words)", - "gasUsed": 2655 - }, - { - "file": "test/Gas.t.sol", - "test": "testCompareStorageSolidity", + "test": "testCompareStorageWriteSolidity", "name": "solidity storage write (cold, 1 word)", "gasUsed": 22107 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageSolidity", + "test": "testCompareStorageWriteSolidity", "name": "solidity storage write (cold, 1 word, partial)", "gasUsed": 22136 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageSolidity", + "test": "testCompareStorageWriteSolidity", "name": "solidity storage write (cold, 10 words)", "gasUsed": 199902 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageSolidity", + "test": "testCompareStorageWriteSolidity", "name": "solidity storage write (warm, 1 word)", "gasUsed": 107 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageSolidity", + "test": "testCompareStorageWriteSolidity", "name": "solidity storage write (warm, 1 word, partial)", "gasUsed": 236 }, { "file": "test/Gas.t.sol", - "test": "testCompareStorageSolidity", + "test": "testCompareStorageWriteSolidity", "name": "solidity storage write (warm, 10 words)", "gasUsed": 1988 }, { - "file": "test/Gas.t.sol", - "test": "testCompareStorageSolidity", + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadMUD", + "name": "MUD storage load (cold, 1 word)", + "gasUsed": 2411 + }, + { + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadMUD", + "name": "MUD storage load (cold, 1 word, partial)", + "gasUsed": 2460 + }, + { + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadMUD", + "name": "MUD storage load (cold, 10 words)", + "gasUsed": 19911 + }, + { + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadMUD", + "name": "MUD storage load (warm, 1 word)", + "gasUsed": 412 + }, + { + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadMUD", + "name": "MUD storage load (warm, 1 word, partial)", + "gasUsed": 460 + }, + { + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadMUD", + "name": "MUD storage load (warm, 10 words)", + "gasUsed": 1914 + }, + { + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadSolidity", + "name": "solidity storage load (cold, 1 word)", + "gasUsed": 2109 + }, + { + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadSolidity", + "name": "solidity storage load (cold, 1 word, partial)", + "gasUsed": 2126 + }, + { + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadSolidity", + "name": "solidity storage load (cold, 10 words)", + "gasUsed": 19894 + }, + { + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadSolidity", "name": "solidity storage load (warm, 1 word)", "gasUsed": 109 }, { - "file": "test/Gas.t.sol", - "test": "testCompareStorageSolidity", + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadSolidity", "name": "solidity storage load (warm, 1 word, partial)", "gasUsed": 126 }, { - "file": "test/Gas.t.sol", - "test": "testCompareStorageSolidity", + "file": "test/GasStorageLoad.t.sol", + "test": "testCompareStorageLoadSolidity", "name": "solidity storage load (warm, 10 words)", - "gasUsed": 1916 + "gasUsed": 1897 }, { "file": "test/KeyEncoding.t.sol", @@ -225,13 +261,13 @@ "file": "test/Mixed.t.sol", "test": "testSetAndGet", "name": "set record in Mixed", - "gasUsed": 111108 + "gasUsed": 110779 }, { "file": "test/Mixed.t.sol", "test": "testSetAndGet", "name": "get record from Mixed", - "gasUsed": 12600 + "gasUsed": 12438 }, { "file": "test/PackedCounter.t.sol", @@ -381,43 +417,43 @@ "file": "test/Storage.t.sol", "test": "testStoreLoad", "name": "store 1 storage slot", - "gasUsed": 22503 + "gasUsed": 22339 }, { "file": "test/Storage.t.sol", "test": "testStoreLoad", "name": "store 34 bytes over 3 storage slots (with offset and safeTrail))", - "gasUsed": 23158 + "gasUsed": 23009 }, { "file": "test/Storage.t.sol", "test": "testStoreLoad", "name": "load 34 bytes over 3 storage slots (with offset and safeTrail))", - "gasUsed": 1099 + "gasUsed": 865 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testGetFieldSlice", "name": "get field slice (cold, 1 slot)", - "gasUsed": 8152 + "gasUsed": 7996 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testGetFieldSlice", "name": "get field slice (warm, 1 slot)", - "gasUsed": 2180 + "gasUsed": 2065 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testGetFieldSlice", "name": "get field slice (semi-cold, 1 slot)", - "gasUsed": 4185 + "gasUsed": 4070 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testGetFieldSlice", "name": "get field slice (warm, 2 slots)", - "gasUsed": 4471 + "gasUsed": 4296 }, { "file": "test/StoreCoreDynamic.t.sol", @@ -447,43 +483,43 @@ "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromSecondField", "name": "pop from field (cold, 1 slot, 1 uint32 item)", - "gasUsed": 30792 + "gasUsed": 30424 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromSecondField", "name": "pop from field (warm, 1 slot, 1 uint32 item)", - "gasUsed": 18847 + "gasUsed": 18479 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromThirdField", "name": "pop from field (cold, 2 slots, 10 uint32 items)", - "gasUsed": 32675 + "gasUsed": 32244 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromThirdField", "name": "pop from field (warm, 2 slots, 10 uint32 items)", - "gasUsed": 18730 + "gasUsed": 18299 }, { "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access non-existing record", - "gasUsed": 7321 + "gasUsed": 7267 }, { "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access static field of non-existing record", - "gasUsed": 2914 + "gasUsed": 2758 }, { "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access dynamic field of non-existing record", - "gasUsed": 3587 + "gasUsed": 3420 }, { "file": "test/StoreCoreGas.t.sol", @@ -495,13 +531,13 @@ "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access slice of dynamic field of non-existing record", - "gasUsed": 1324 + "gasUsed": 1202 }, { "file": "test/StoreCoreGas.t.sol", "test": "testDeleteData", "name": "delete record (complex data, 3 slots)", - "gasUsed": 10447 + "gasUsed": 10213 }, { "file": "test/StoreCoreGas.t.sol", @@ -519,61 +555,61 @@ "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "register subscriber", - "gasUsed": 66466 + "gasUsed": 66065 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "set record on table with subscriber", - "gasUsed": 74409 + "gasUsed": 73933 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "set static field on table with subscriber", - "gasUsed": 30350 + "gasUsed": 29860 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "delete record on table with subscriber", - "gasUsed": 23011 + "gasUsed": 22510 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "register subscriber", - "gasUsed": 66466 + "gasUsed": 66065 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "set (dynamic) record on table with subscriber", - "gasUsed": 167846 + "gasUsed": 167262 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "set (dynamic) field on table with subscriber", - "gasUsed": 33500 + "gasUsed": 32942 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "delete (dynamic) record on table with subscriber", - "gasUsed": 24483 + "gasUsed": 23977 }, { "file": "test/StoreCoreGas.t.sol", "test": "testPushToField", "name": "push to field (1 slot, 1 uint32 item)", - "gasUsed": 16518 + "gasUsed": 16124 }, { "file": "test/StoreCoreGas.t.sol", "test": "testPushToField", "name": "push to field (2 slots, 10 uint32 items)", - "gasUsed": 39234 + "gasUsed": 38780 }, { "file": "test/StoreCoreGas.t.sol", @@ -597,13 +633,13 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetDynamicData", "name": "set complex record with dynamic data (4 slots)", - "gasUsed": 109160 + "gasUsed": 108831 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetDynamicData", "name": "get complex record with dynamic data (4 slots)", - "gasUsed": 6442 + "gasUsed": 6280 }, { "file": "test/StoreCoreGas.t.sol", @@ -639,103 +675,103 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set static field (1 slot)", - "gasUsed": 39378 + "gasUsed": 39144 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "get static field (1 slot)", - "gasUsed": 2915 + "gasUsed": 2759 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set static field (overlap 2 slot)", - "gasUsed": 34340 + "gasUsed": 34091 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "get static field (overlap 2 slot)", - "gasUsed": 3755 + "gasUsed": 3580 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set dynamic field (1 slot, first dynamic field)", - "gasUsed": 56839 + "gasUsed": 56571 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "get dynamic field (1 slot, first dynamic field)", - "gasUsed": 3811 + "gasUsed": 3610 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set dynamic field (1 slot, second dynamic field)", - "gasUsed": 34971 + "gasUsed": 34704 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "get dynamic field (1 slot, second dynamic field)", - "gasUsed": 3826 + "gasUsed": 3625 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticData", "name": "set static record (1 slot)", - "gasUsed": 38827 + "gasUsed": 38606 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticData", "name": "get static record (1 slot)", - "gasUsed": 1320 + "gasUsed": 1266 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticDataSpanningWords", "name": "set static record (2 slots)", - "gasUsed": 61395 + "gasUsed": 61111 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticDataSpanningWords", "name": "get static record (2 slots)", - "gasUsed": 1569 + "gasUsed": 1455 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetMetadata", "name": "StoreCore: set table metadata", - "gasUsed": 252784 + "gasUsed": 251951 }, { "file": "test/StoreCoreGas.t.sol", "test": "testUpdateInField", "name": "update in field (1 slot, 1 uint32 item)", - "gasUsed": 16064 + "gasUsed": 15670 }, { "file": "test/StoreCoreGas.t.sol", "test": "testUpdateInField", "name": "push to field (2 slots, 6 uint64 items)", - "gasUsed": 17155 + "gasUsed": 16641 }, { "file": "test/StoreMetadata.t.sol", "test": "testSetAndGet", "name": "set record in StoreMetadataTable", - "gasUsed": 251257 + "gasUsed": 250424 }, { "file": "test/StoreMetadata.t.sol", "test": "testSetAndGet", "name": "get record from StoreMetadataTable", - "gasUsed": 12018 + "gasUsed": 11430 }, { "file": "test/StoreSwitch.t.sol", @@ -753,37 +789,37 @@ "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "set field in Callbacks", - "gasUsed": 62243 + "gasUsed": 61975 }, { "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "get field from Callbacks (warm)", - "gasUsed": 5305 + "gasUsed": 5104 }, { "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "push field to Callbacks", - "gasUsed": 39931 + "gasUsed": 39481 }, { "file": "test/tables/Hooks.t.sol", "test": "testSetAndGet", "name": "set field in Hooks", - "gasUsed": 64400 + "gasUsed": 64132 }, { "file": "test/tables/Hooks.t.sol", "test": "testSetAndGet", "name": "get field from Hooks (warm)", - "gasUsed": 5455 + "gasUsed": 5254 }, { "file": "test/tables/Hooks.t.sol", "test": "testSetAndGet", "name": "push field to Hooks", - "gasUsed": 39922 + "gasUsed": 39472 }, { "file": "test/tightcoder/DecodeSlice.t.sol", @@ -849,12 +885,12 @@ "file": "test/Vector2.t.sol", "test": "testSetAndGet", "name": "set Vector2 record", - "gasUsed": 37646 + "gasUsed": 37425 }, { "file": "test/Vector2.t.sol", "test": "testSetAndGet", "name": "get Vector2 record", - "gasUsed": 4457 + "gasUsed": 4403 } ] diff --git a/packages/store/src/Storage.sol b/packages/store/src/Storage.sol index 403de0490a..0961cf4d20 100644 --- a/packages/store/src/Storage.sol +++ b/packages/store/src/Storage.sol @@ -2,16 +2,9 @@ pragma solidity >=0.8.0; import { leftMask } from "./Utils.sol"; +import { Memory } from "./Memory.sol"; -/** - * TODO Probably not fully optimized - * (see https://github.com/latticexyz/mud/issues/444) - */ library Storage { - function store(uint256 storagePointer, bytes memory data) internal { - store(storagePointer, 0, data); - } - function store(uint256 storagePointer, bytes32 data) internal { assembly { sstore(storagePointer, data) @@ -19,59 +12,59 @@ library Storage { } function store(uint256 storagePointer, uint256 offset, bytes memory data) internal { - uint256 memoryPointer; - assembly { - memoryPointer := add(data, 0x20) - } - store(storagePointer, offset, memoryPointer, data.length); + store(storagePointer, offset, Memory.dataPointer(data), data.length); } /** - * @notice Stores raw bytes to storage at the given storagePointer and offset (keeping the rest of the word intact) + * Stores raw bytes to storage at the given storagePointer and offset (keeping the rest of the word intact) */ function store(uint256 storagePointer, uint256 offset, uint256 memoryPointer, uint256 length) internal { - // Support offsets that are greater than 32 bytes by incrementing the storagePointer and decrementing the offset - unchecked { - storagePointer += offset / 32; - offset %= 32; - } - - // For the first word, if there is an offset, apply a mask to beginning if (offset > 0) { - // Get the word's remaining length after the offset - uint256 wordRemainder; - // (safe because of `offset %= 32` at the start) - unchecked { - wordRemainder = 32 - offset; + // Support offsets that are greater than 32 bytes by incrementing the storagePointer and decrementing the offset + if (offset >= 32) { + unchecked { + storagePointer += offset / 32; + offset %= 32; + } } - uint256 mask = leftMask(length); - /// @solidity memory-safe-assembly - assembly { - // Load data from memory and offset it to match storage - let bitOffset := mul(offset, 8) - mask := shr(bitOffset, mask) - let offsetData := shr(bitOffset, mload(memoryPointer)) - - sstore( - storagePointer, - or( - // Store the middle part - and(offsetData, mask), - // Preserve the surrounding parts - and(sload(storagePointer), not(mask)) + // For the first word, if there is an offset, apply a mask to beginning + if (offset > 0) { + // Get the word's remaining length after the offset + uint256 wordRemainder; + // (safe because of `offset %= 32` at the start) + unchecked { + wordRemainder = 32 - offset; + } + + uint256 mask = leftMask(length); + /// @solidity memory-safe-assembly + assembly { + // Load data from memory and offset it to match storage + let bitOffset := mul(offset, 8) + mask := shr(bitOffset, mask) + let offsetData := shr(bitOffset, mload(memoryPointer)) + + sstore( + storagePointer, + or( + // Store the middle part + and(offsetData, mask), + // Preserve the surrounding parts + and(sload(storagePointer), not(mask)) + ) ) - ) - } - // Return if done - if (length <= wordRemainder) return; - - // Advance pointers - storagePointer += 1; - // (safe because of `length <= prefixLength` earlier) - unchecked { - memoryPointer += wordRemainder; - length -= wordRemainder; + } + // Return if done + if (length <= wordRemainder) return; + + // Advance pointers + // (safe because of `length <= wordRemainder` earlier) + unchecked { + storagePointer += 1; + memoryPointer += wordRemainder; + length -= wordRemainder; + } } } @@ -81,9 +74,8 @@ library Storage { assembly { sstore(storagePointer, mload(memoryPointer)) } - storagePointer += 1; - // (safe unless length is improbably large) unchecked { + storagePointer += 1; memoryPointer += 32; length -= 32; } @@ -113,69 +105,77 @@ library Storage { } } - function load(uint256 storagePointer, uint256 length) internal view returns (bytes memory) { - return load(storagePointer, length, 0); - } - /** - * @notice Load raw bytes from storage at the given storagePointer, offset, and length + * Load raw bytes from storage at the given storagePointer, offset, and length */ function load(uint256 storagePointer, uint256 length, uint256 offset) internal view returns (bytes memory result) { - // TODO this will probably use less gas via manual memory allocation - // (see https://github.com/latticexyz/mud/issues/444) - result = new bytes(length); uint256 memoryPointer; + /// @solidity memory-safe-assembly assembly { + // Solidity's YulUtilFunctions::roundUpFunction + function round_up_to_mul_of_32(value) -> _result { + _result := and(add(value, 31), not(31)) + } + + // Allocate memory + result := mload(0x40) memoryPointer := add(result, 0x20) + mstore(0x40, round_up_to_mul_of_32(add(memoryPointer, length))) + // Store length + mstore(result, length) } load(storagePointer, length, offset, memoryPointer); return result; } /** - * @notice Append raw bytes from storage at the given storagePointer, offset, and length to the given memoryPointer + * Append raw bytes from storage at the given storagePointer, offset, and length to the given memoryPointer */ function load(uint256 storagePointer, uint256 length, uint256 offset, uint256 memoryPointer) internal view { - // Support offsets that are greater than 32 bytes by incrementing the storagePointer and decrementing the offset - unchecked { - storagePointer += offset / 32; - offset %= 32; - } - - // For the first word, if there is an offset, apply a mask to beginning if (offset > 0) { - // Get the word's remaining length after the offset - uint256 wordRemainder; - // (safe because of `offset %= 32` at the start) - unchecked { - wordRemainder = 32 - offset; + // Support offsets that are greater than 32 bytes by incrementing the storagePointer and decrementing the offset + if (offset >= 32) { + unchecked { + storagePointer += offset / 32; + offset %= 32; + } } - uint256 mask = leftMask(wordRemainder); - /// @solidity memory-safe-assembly - assembly { - // Load data from storage and offset it to match memory - let offsetData := shl(mul(offset, 8), sload(storagePointer)) - - mstore( - memoryPointer, - or( - // store the middle part - and(offsetData, mask), - // preserve the surrounding parts - and(mload(memoryPointer), not(mask)) + // For the first word, if there is an offset, apply a mask to beginning + if (offset > 0) { + // Get the word's remaining length after the offset + uint256 wordRemainder; + // (safe because of `offset %= 32` at the start) + unchecked { + wordRemainder = 32 - offset; + } + + uint256 mask = leftMask(wordRemainder); + /// @solidity memory-safe-assembly + assembly { + // Load data from storage and offset it to match memory + let offsetData := shl(mul(offset, 8), sload(storagePointer)) + + mstore( + memoryPointer, + or( + // store the middle part + and(offsetData, mask), + // preserve the surrounding parts + and(mload(memoryPointer), not(mask)) + ) ) - ) - } - // Return if done - if (length <= wordRemainder) return; - - // Advance pointers - storagePointer += 1; - // (safe because of `length <= prefixLength` earlier) - unchecked { - memoryPointer += wordRemainder; - length -= wordRemainder; + } + // Return if done + if (length <= wordRemainder) return; + + // Advance pointers + // (safe because of `length <= wordRemainder` earlier) + unchecked { + storagePointer += 1; + memoryPointer += wordRemainder; + length -= wordRemainder; + } } } @@ -185,9 +185,8 @@ library Storage { assembly { mstore(memoryPointer, sload(storagePointer)) } - storagePointer += 1; - // (safe unless length is improbably large) unchecked { + storagePointer += 1; memoryPointer += 32; length -= 32; } diff --git a/packages/store/src/StoreCore.sol b/packages/store/src/StoreCore.sol index af8a8c6b71..251d5c7785 100644 --- a/packages/store/src/StoreCore.sol +++ b/packages/store/src/StoreCore.sol @@ -607,7 +607,7 @@ library StoreCoreInternal { // Store the provided value in storage uint256 dynamicDataLocation = _getDynamicDataLocation(tableId, key, dynamicSchemaIndex); - Storage.store({ storagePointer: dynamicDataLocation, data: data }); + Storage.store({ storagePointer: dynamicDataLocation, offset: 0, data: data }); } function _pushToDynamicField( @@ -731,7 +731,7 @@ library StoreCoreInternal { uint256 location = _getDynamicDataLocation(tableId, key, dynamicSchemaIndex); uint256 dataLength = _loadEncodedDynamicDataLength(tableId, key).atIndex(dynamicSchemaIndex); - return Storage.load({ storagePointer: location, length: dataLength }); + return Storage.load({ storagePointer: location, length: dataLength, offset: 0 }); } /************************************************************************ diff --git a/packages/store/test/Gas.t.sol b/packages/store/test/Gas.t.sol index 5d8c719976..884b019ebd 100644 --- a/packages/store/test/Gas.t.sol +++ b/packages/store/test/Gas.t.sol @@ -50,7 +50,7 @@ contract GasTest is Test, GasReporter { assertEq(abi.encode(abiDecoded), abi.encode(customDecoded)); } - function testCompareStorageSolidity() public { + function testCompareStorageWriteSolidity() public { (bytes32 valueSimple, bytes16 valuePartial, bytes memory value9Words) = SolidityStorage.generateValues(); ( SolidityStorage.LayoutSimple storage layoutSimple, @@ -85,30 +85,13 @@ contract GasTest is Test, GasReporter { startGasReport("solidity storage write (warm, 10 words)"); layoutBytes.value = value9Words; endGasReport(); - - // load - - startGasReport("solidity storage load (warm, 1 word)"); - valueSimple = layoutSimple.value; - endGasReport(); - - startGasReport("solidity storage load (warm, 1 word, partial)"); - valuePartial = layoutPartial.value; - endGasReport(); - - startGasReport("solidity storage load (warm, 10 words)"); - value9Words = layoutBytes.value; - endGasReport(); - - // Do something in case the optimizer removes unused assignments - someContract.doSomethingWithBytes(abi.encode(valueSimple, valuePartial, value9Words)); } // Note that this compares storage writes in isolation, which can be misleading, // since MUD encoding is dynamic and separate from storage, // but solidity encoding is hardcoded at compile-time and is part of writing to storage. // (look for comparison of native storage vs MUD tables for a more comprehensive overview) - function testCompareStorageMUD() public { + function testCompareStorageWriteMUD() public { (bytes32 valueSimple, bytes16 valuePartial, bytes memory value9Words) = SolidityStorage.generateValues(); bytes memory encodedSimple = abi.encodePacked(valueSimple); bytes memory encodedPartial = abi.encodePacked(valuePartial); @@ -141,23 +124,6 @@ contract GasTest is Test, GasReporter { startGasReport("MUD storage write (warm, 10 words)"); Storage.store(SolidityStorage.STORAGE_SLOT_BYTES, 0, encoded9Words); endGasReport(); - - // load - - startGasReport("MUD storage load (warm, 1 word)"); - encodedSimple = Storage.load(SolidityStorage.STORAGE_SLOT_SIMPLE, encodedSimple.length, 0); - endGasReport(); - - startGasReport("MUD storage load (warm, 1 word, partial)"); - encodedPartial = Storage.load(SolidityStorage.STORAGE_SLOT_PARTIAL, encodedPartial.length, 16); - endGasReport(); - - startGasReport("MUD storage load (warm, 10 words)"); - encoded9Words = Storage.load(SolidityStorage.STORAGE_SLOT_BYTES, encoded9Words.length, 0); - endGasReport(); - - // Do something in case the optimizer removes unused assignments - someContract.doSomethingWithBytes(abi.encode(encodedSimple, encodedPartial, encoded9Words)); } } diff --git a/packages/store/test/GasStorageLoad.t.sol b/packages/store/test/GasStorageLoad.t.sol new file mode 100644 index 0000000000..3991d25e12 --- /dev/null +++ b/packages/store/test/GasStorageLoad.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { Test } from "forge-std/Test.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; +import { Storage } from "../src/Storage.sol"; +import { SolidityStorage, SomeContract } from "./Gas.t.sol"; + +contract GasStorageLoadTest is Test, GasReporter { + SomeContract someContract = new SomeContract(); + + // Cold loads require us to initialize the values outside of their test + function setUp() public { + (bytes32 valueSimple, bytes16 valuePartial, bytes memory value9Words) = SolidityStorage.generateValues(); + ( + SolidityStorage.LayoutSimple storage layoutSimple, + SolidityStorage.LayoutPartial storage layoutPartial, + SolidityStorage.LayoutBytes storage layoutBytes + ) = SolidityStorage.layouts(); + + layoutSimple.value = valueSimple; + layoutPartial.value = valuePartial; + layoutBytes.value = value9Words; + } + + function testCompareStorageLoadSolidity() public { + (bytes32 valueSimple, bytes16 valuePartial, bytes memory value9Words) = SolidityStorage.generateValues(); + ( + SolidityStorage.LayoutSimple storage layoutSimple, + SolidityStorage.LayoutPartial storage layoutPartial, + SolidityStorage.LayoutBytes storage layoutBytes + ) = SolidityStorage.layouts(); + + startGasReport("solidity storage load (cold, 1 word)"); + valueSimple = layoutSimple.value; + endGasReport(); + + startGasReport("solidity storage load (cold, 1 word, partial)"); + valuePartial = layoutPartial.value; + endGasReport(); + + startGasReport("solidity storage load (cold, 10 words)"); + value9Words = layoutBytes.value; + endGasReport(); + + // warm + + startGasReport("solidity storage load (warm, 1 word)"); + valueSimple = layoutSimple.value; + endGasReport(); + + startGasReport("solidity storage load (warm, 1 word, partial)"); + valuePartial = layoutPartial.value; + endGasReport(); + + startGasReport("solidity storage load (warm, 10 words)"); + value9Words = layoutBytes.value; + endGasReport(); + + // Do something in case the optimizer removes unused assignments + someContract.doSomethingWithBytes(abi.encode(valueSimple, valuePartial, value9Words)); + } + + function testCompareStorageLoadMUD() public { + (bytes32 valueSimple, bytes16 valuePartial, bytes memory value9Words) = SolidityStorage.generateValues(); + bytes memory encodedSimple = abi.encodePacked(valueSimple); + bytes memory encodedPartial = abi.encodePacked(valuePartial); + bytes memory encoded9Words = abi.encodePacked(value9Words.length, value9Words); + + startGasReport("MUD storage load (cold, 1 word)"); + encodedSimple = Storage.load(SolidityStorage.STORAGE_SLOT_SIMPLE, encodedSimple.length, 0); + endGasReport(); + + startGasReport("MUD storage load (cold, 1 word, partial)"); + encodedPartial = Storage.load(SolidityStorage.STORAGE_SLOT_PARTIAL, encodedPartial.length, 16); + endGasReport(); + + startGasReport("MUD storage load (cold, 10 words)"); + encoded9Words = Storage.load(SolidityStorage.STORAGE_SLOT_BYTES, encoded9Words.length, 0); + endGasReport(); + + // warm + + startGasReport("MUD storage load (warm, 1 word)"); + encodedSimple = Storage.load(SolidityStorage.STORAGE_SLOT_SIMPLE, encodedSimple.length, 0); + endGasReport(); + + startGasReport("MUD storage load (warm, 1 word, partial)"); + encodedPartial = Storage.load(SolidityStorage.STORAGE_SLOT_PARTIAL, encodedPartial.length, 16); + endGasReport(); + + startGasReport("MUD storage load (warm, 10 words)"); + encoded9Words = Storage.load(SolidityStorage.STORAGE_SLOT_BYTES, encoded9Words.length, 0); + endGasReport(); + + // Do something in case the optimizer removes unused assignments + someContract.doSomethingWithBytes(abi.encode(encodedSimple, encodedPartial, encoded9Words)); + } +} diff --git a/packages/store/test/Storage.t.sol b/packages/store/test/Storage.t.sol index fe8be775ed..1d6005aa6d 100644 --- a/packages/store/test/Storage.t.sol +++ b/packages/store/test/Storage.t.sol @@ -27,10 +27,10 @@ contract StorageTest is Test, GasReporter { // First store some data to storage at the target slot and two slots after the target slot startGasReport("store 1 storage slot"); - Storage.store({ storagePointer: storagePointer, data: originalDataFirstSlot }); + Storage.store({ storagePointer: storagePointer, offset: 0, data: originalDataFirstSlot }); endGasReport(); - Storage.store({ storagePointer: storagePointerTwoSlotsAfter, data: originalDataLastSlot }); + Storage.store({ storagePointer: storagePointerTwoSlotsAfter, offset: 0, data: originalDataLastSlot }); // Then set the target slot, partially overwriting the first and third slot, but using safeTrail and offset diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index 36f0745446..e7a68eba2d 100644 --- a/packages/world/gas-report.json +++ b/packages/world/gas-report.json @@ -3,73 +3,73 @@ "file": "test/KeysInTableModule.t.sol", "test": "testInstallComposite", "name": "install keys in table module", - "gasUsed": 1255918 + "gasUsed": 1248704 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1255918 + "gasUsed": 1248704 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "set a record on a table with keysInTableModule installed", - "gasUsed": 189004 + "gasUsed": 187021 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1255918 + "gasUsed": 1248704 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1255918 + "gasUsed": 1248704 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "change a composite record on a table with keysInTableModule installed", - "gasUsed": 28442 + "gasUsed": 27801 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "delete a composite record on a table with keysInTableModule installed", - "gasUsed": 270004 + "gasUsed": 262682 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1255918 + "gasUsed": 1248704 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "change a record on a table with keysInTableModule installed", - "gasUsed": 27056 + "gasUsed": 26415 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "delete a record on a table with keysInTableModule installed", - "gasUsed": 141266 + "gasUsed": 137742 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 606592 + "gasUsed": 602496 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "Get list of keys with a given value", - "gasUsed": 7716 + "gasUsed": 7444 }, { "file": "test/KeysWithValueModule.t.sol", @@ -81,240 +81,240 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 606592 + "gasUsed": 602496 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "set a record on a table with KeysWithValueModule installed", - "gasUsed": 162059 + "gasUsed": 159927 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 606592 + "gasUsed": 602496 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "change a record on a table with KeysWithValueModule installed", - "gasUsed": 129713 + "gasUsed": 127327 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "delete a record on a table with KeysWithValueModule installed", - "gasUsed": 50859 + "gasUsed": 49418 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 606592 + "gasUsed": 602496 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "set a field on a table with KeysWithValueModule installed", - "gasUsed": 170334 + "gasUsed": 168075 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "change a field on a table with KeysWithValueModule installed", - "gasUsed": 132692 + "gasUsed": 130339 }, { "file": "test/query.t.sol", "test": "testCombinedHasHasValueNotQuery", "name": "CombinedHasHasValueNotQuery", - "gasUsed": 163160 + "gasUsed": 160604 }, { "file": "test/query.t.sol", "test": "testCombinedHasHasValueQuery", "name": "CombinedHasHasValueQuery", - "gasUsed": 77029 + "gasUsed": 75381 }, { "file": "test/query.t.sol", "test": "testCombinedHasNotQuery", "name": "CombinedHasNotQuery", - "gasUsed": 222474 + "gasUsed": 220085 }, { "file": "test/query.t.sol", "test": "testCombinedHasQuery", "name": "CombinedHasQuery", - "gasUsed": 139829 + "gasUsed": 138370 }, { "file": "test/query.t.sol", "test": "testCombinedHasValueNotQuery", "name": "CombinedHasValueNotQuery", - "gasUsed": 139055 + "gasUsed": 137295 }, { "file": "test/query.t.sol", "test": "testCombinedHasValueQuery", "name": "CombinedHasValueQuery", - "gasUsed": 19621 + "gasUsed": 19014 }, { "file": "test/query.t.sol", "test": "testHasQuery", "name": "HasQuery", - "gasUsed": 31559 + "gasUsed": 31224 }, { "file": "test/query.t.sol", "test": "testHasQuery1000Keys", "name": "HasQuery with 1000 keys", - "gasUsed": 10806607 + "gasUsed": 10687510 }, { "file": "test/query.t.sol", "test": "testHasQuery100Keys", "name": "HasQuery with 100 keys", - "gasUsed": 1009010 + "gasUsed": 997013 }, { "file": "test/query.t.sol", "test": "testHasValueQuery", "name": "HasValueQuery", - "gasUsed": 9522 + "gasUsed": 9187 }, { "file": "test/query.t.sol", "test": "testNotValueQuery", "name": "NotValueQuery", - "gasUsed": 70209 + "gasUsed": 68939 }, { "file": "test/SnapSyncModule.t.sol", "test": "testSnapSyncGas", "name": "Call snap sync on a table with 1 record", - "gasUsed": 39819 + "gasUsed": 39143 }, { "file": "test/SnapSyncModule.t.sol", "test": "testSnapSyncGas", "name": "Call snap sync on a table with 2 records", - "gasUsed": 57129 + "gasUsed": 56220 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 797781 + "gasUsed": 791266 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "get a unique entity nonce (non-root module)", - "gasUsed": 71099 + "gasUsed": 70073 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 769225 + "gasUsed": 763358 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "get a unique entity nonce (root module)", - "gasUsed": 71099 + "gasUsed": 70073 }, { "file": "test/World.t.sol", "test": "testDeleteRecord", "name": "Delete record", - "gasUsed": 15060 + "gasUsed": 14659 }, { "file": "test/World.t.sol", "test": "testPushToField", "name": "Push data to the table", - "gasUsed": 95419 + "gasUsed": 94777 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a fallback system", - "gasUsed": 81644 + "gasUsed": 80591 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a root fallback system", - "gasUsed": 72828 + "gasUsed": 71771 }, { "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 102239 + "gasUsed": 101182 }, { "file": "test/World.t.sol", "test": "testRegisterNamespace", "name": "Register a new namespace", - "gasUsed": 157623 + "gasUsed": 156426 }, { "file": "test/World.t.sol", "test": "testRegisterRootFunctionSelector", "name": "Register a root function selector", - "gasUsed": 88731 + "gasUsed": 87674 }, { "file": "test/World.t.sol", "test": "testRegisterTable", "name": "Register a new table in the namespace", - "gasUsed": 255729 + "gasUsed": 253992 }, { "file": "test/World.t.sol", "test": "testSetField", "name": "Write data to a table field", - "gasUsed": 43649 + "gasUsed": 43248 }, { "file": "test/World.t.sol", "test": "testSetMetadata", "name": "Set metadata", - "gasUsed": 279560 + "gasUsed": 278225 }, { "file": "test/World.t.sol", "test": "testSetRecord", "name": "Write data to the table", - "gasUsed": 41504 + "gasUsed": 41103 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testPopFromField", "name": "pop 1 address (cold)", - "gasUsed": 38906 + "gasUsed": 38297 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testPopFromField", "name": "pop 1 address (warm)", - "gasUsed": 21696 + "gasUsed": 21087 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testUpdateInField", "name": "updateInField 1 item (cold)", - "gasUsed": 41285 + "gasUsed": 40609 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testUpdateInField", "name": "updateInField 1 item (warm)", - "gasUsed": 24487 + "gasUsed": 23811 } ]