-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathVirtualStruct.sol
More file actions
237 lines (185 loc) · 11.9 KB
/
VirtualStruct.sol
File metadata and controls
237 lines (185 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
pragma solidity ^0.4.19;
/*
Author: Chance Santana-Wees
Contact Email: figs999@gmail.com
This sample outlines a technique for creating a stack-only "Virtual Struct" which fits in a single 256bit EVM word.
There are two primary benefits to using this technique rather than using a solidity struct:
1) Stack only! (saves gas) The virtual structs never go into memory or storage unless explicitly
told to do so. I'm not a big fan of how liberaly the solidity compiler uses memory, and swapping
things in and out of memory when it isn't needed is an avoidable gas drain.
2) More flexibility when compacting data. You are not bound to solidity's built-in data types and
can jam as much as possible into a single stack slot. Want to use 91 bits for something? Go ahead.
Comparitive Gas Costs of Below Contract Methods:
Method Virtual Struct Cost Normal Struct Cost Optimizer Enabled
PlaceADiceBet 43788 70278 No
LogBetProperties 11040 12281 No
PlaceADiceBet 43249 69362 Yes
LogBetProperties 11049 10856 Yes
Notes:
My first version was compiled without the optimizer, which led me to believe that using the Virtual Struct method
with a library would lead to extra overhead due to using delegatecall. Thanks to @chriseth for pointing out that
internal library calls shouldn't do this if the optimizer is turned on.
I left the unoptimized gas costs in for comparison, mainly because I found it intriguing that the method
which used the library actually DID add a tiny bit of overhead anyway and that the optimizer made the
act of reading a property from a solidity struct slightly more efficient than my method.
Now I just need to comb through the opcodes of the optimized version of the built-in struct read to see
what it's doing differently! I must be wasting some gas somewhere...
Also, I'm mulling on if the 9 gas it apparently costs to call a library method (extra jumps maybe?) is worth
cleaner looking code. I mean at current gas prices that's pretty negligible... but if the goal is to get the
gas cost as low as possible...
*/
//A Sample "Virtual Struct"
library BetRecordLib {
enum GameType { DrawPoker, SimpleDice, BlackJack, Keno }
/**--Virtual Structure-----------
* BetRecord
* Type Name Index Bits
uint16 PlayerID; 0 16 //Index of Player address in an array with max capacity of 65,535
uint26 RevealBlock; 16 26 //This contract will fail in ~30 years... eh.
GameType Type; 42 2 //Yes. 2. There is no reason that enum should take 256 bits!!!
bytes15 WagerData; 44 120 //Biggest game is Keno, needs 6 bits per choice.
bool DidPayOut; 164 1 //Has the player already been paid?
uint... WageredWei; 165 91 //left over bits can hold wager of up to ~2.5 billion ether. Should be plenty.
**-------------------------------*/
/**Here we create bit masks for all of our funky data sizes*/
uint private constant mask1 = 1; //binary 1
uint private constant mask2 = (1 << 2) -1; //binary 11
uint private constant mask16 = (1 << 16) -1; //binary 1111 1111 1111 1111
uint private constant mask26 = (1 << 26) -1; //etc...
uint private constant mask91 = (1 << 91) -1;
uint private constant mask120 = (1 << 120)-1;
/**Here we create shift indices for each property. It is simply 1 shifted left by the Index listed above.*/
uint private constant _PlayerID = 1 << 0;
uint private constant _RevealBlock = 1 << 16;
uint private constant _Type = 1 << 42;
uint private constant _WagerData = 1 << 44;
uint private constant _DidPayOut = 1 << 164;
uint private constant _WageredWei = 1 << 165;
/**Generic Getter/Setter which does the bit magic.*/
function GetProperty(bytes32 BetRecord, uint mask, uint shift) private pure returns (uint property) {
property = mask&(uint(BetRecord)/shift);
}
function SetProperty(bytes32 BetRecord, uint mask, uint shift, uint value) private pure returns (bytes32 updated) {
updated = bytes32((~(mask*shift) & uint(BetRecord)) | ((value & mask) * shift));
}
/**--Getter Pattern--------------
function <Property> (bytes32 BetRecord) internal pure returns (<Type>) { return <Type>( GetProperty( BetRecord, <Mask>, <Shift>)); }
*
**--Setter Pattern--------------
function <Property> (bytes32 BetRecord, <Type> value) internal pure returns (bytes32) { return SetProperty( BetRecord, <Mask>, <Shift>, uint(value) ); }
*/
function GetPlayerID (bytes32 BetRecord) internal pure returns (uint16) { return uint16( GetProperty( BetRecord, mask16, _PlayerID)); }
function SetPlayerID (bytes32 BetRecord, uint16 value) internal pure returns (bytes32) { return SetProperty( BetRecord, mask16, _PlayerID, value ); }
function GetRevealBlock (bytes32 BetRecord) internal pure returns (uint32) { return uint32( GetProperty( BetRecord, mask26, _RevealBlock)); }
function SetRevealBlock (bytes32 BetRecord, uint32 value) internal pure returns (bytes32) { return SetProperty( BetRecord, mask26, _RevealBlock, value ); }
function GetType (bytes32 BetRecord) internal pure returns (GameType){ return GameType( GetProperty( BetRecord, mask2, _Type)); }
function SetType (bytes32 BetRecord, GameType value) internal pure returns (bytes32) { return SetProperty( BetRecord, mask2, _Type, uint(value) ); }
function GetWagerData (bytes32 BetRecord) internal pure returns (bytes15) { return bytes15( GetProperty( BetRecord, mask120,_WagerData)); }
function SetWagerData (bytes32 BetRecord, bytes15 value) internal pure returns (bytes32) { return SetProperty( BetRecord, mask120,_WagerData, uint(value) ); }
function GetDidPayOut (bytes32 BetRecord) internal pure returns (bool) { return 0 < GetProperty( BetRecord, mask1, _DidPayOut); }
function SetDidPayOut (bytes32 BetRecord, bool value) internal pure returns (bytes32) { return SetProperty( BetRecord, mask1, _DidPayOut, value?1:0 ); }
function GetWageredWei (bytes32 BetRecord) internal pure returns (uint) { return GetProperty( BetRecord, mask91, _WageredWei); }
function SetWageredWei (bytes32 BetRecord, uint value) internal pure returns (bytes32) { return SetProperty( BetRecord, mask91, _WageredWei, value ); }
}
contract UsingAVirtualStruct{
using BetRecordLib for bytes32;
bytes32[] BetRecords;
mapping(address => uint16) player2ID;
address[] registeredPlayers;
function UsingAVirtualStruct() public {
registeredPlayers.push(0x0);
}
event PlayerRegistered(uint16 playerID, address player);
event BetPlaced(bytes32 BetRecord, uint BetRecordID);
function Register() public returns (uint16) {
uint16 playerID = player2ID[msg.sender];
require(playerID == 0 && registeredPlayers.length < 65535);
playerID = uint16(registeredPlayers.length);
registeredPlayers.push(msg.sender);
player2ID[msg.sender] = playerID;
PlayerRegistered(playerID, msg.sender);
return playerID;
}
function PlaceADiceBet(uint8 number) public payable {
uint16 playerID = player2ID[msg.sender];
require(playerID > 0);
bytes32 BetRecord;
BetRecord = BetRecord.SetPlayerID(playerID);
BetRecord = BetRecord.SetRevealBlock(uint32(block.number+10));
BetRecord = BetRecord.SetType(BetRecordLib.GameType.SimpleDice);
BetRecord = BetRecord.SetWagerData(bytes15(number));
BetRecord = BetRecord.SetWageredWei(msg.value);
uint betID = BetRecords.length;
BetRecords.push(BetRecord);
BetPlaced(BetRecord, betID);
}
event LogProperty(string name, uint value);
function LogBetProperties(uint betID) public {
bytes32 BetRecord = BetRecords[betID];
uint pID = BetRecord.GetPlayerID();
uint RevealBlock = BetRecord.GetRevealBlock();
BetRecordLib.GameType gameType = BetRecord.GetType();
bytes15 wagerData = BetRecord.GetWagerData();
uint wager = BetRecord.GetWageredWei();
LogProperty("PlayerID", pID);
LogProperty("RevealBlock", RevealBlock);
LogProperty("Type", uint(gameType));
LogProperty("WagerData", uint(wagerData));
LogProperty("WageredWei", wager);
}
}
contract UsingANormalStruct {
enum GameType { DrawPoker, SimpleDice, BlackJack, Keno }
struct BetRecord {
uint16 PlayerID; //16
uint32 RevealBlock; //48 Our first overhead. We're forced into 32 bits unless we want the contract to fail in 7.5 years.
uint8 Type; //56 Another overhead! We have to use 8 bits to hold a number that's 1-4! Still better than 256-bits for using the enum by typename.
bytes15 WagerData; //176 Luckily this is the same. Unfortunately it has a hard minimum, so we can't shave anything off of it to get back some lost bits.
bool DidPayOut; //177
uint72 WageredWei; //249 Heres where we really got hammered. We have 79 bits available. Now we can only accept bets up to 4722 Ether! Goodbye highrollers! :P
}
BetRecord[] BetRecords;
mapping(address => uint16) player2ID;
address[] registeredPlayers;
function UsingANormalStruct() public {
registeredPlayers.push(0x0);
}
event PlayerRegistered(uint16 playerID, address player);
event BetPlaced(BetRecord betRecord, uint BetRecordID);
function Register() public returns (uint16) {
uint16 playerID = player2ID[msg.sender];
require(playerID == 0 && registeredPlayers.length < 65535);
playerID = uint16(registeredPlayers.length);
registeredPlayers.push(msg.sender);
player2ID[msg.sender] = playerID;
PlayerRegistered(playerID, msg.sender);
return playerID;
}
function PlaceADiceBet(uint8 number) public payable {
uint16 playerID = player2ID[msg.sender];
require(playerID > 0 && msg.value < 1 << 72);
BetRecord memory bet; //Immediately throwing this into memory. Better than accidentally putting it into storage though.
bet.PlayerID = playerID;
bet.RevealBlock = uint32(block.number+10);
bet.Type = uint8(GameType.SimpleDice);
bet.WagerData = bytes15(number);
bet.WageredWei = uint72(msg.value);
uint betID = BetRecords.length;
BetRecords.push(bet);
BetPlaced(bet, betID);
}
event LogProperty(string name, uint value);
function LogBetProperties(uint betID) public {
BetRecord memory bet = BetRecords[betID];
uint pID = bet.PlayerID;
uint RevealBlock = bet.RevealBlock;
GameType gameType = GameType(uint8(bet.Type));
bytes15 wagerData = bet.WagerData;
uint wager = bet.WageredWei;
LogProperty("PlayerID", pID);
LogProperty("RevealBlock", RevealBlock);
LogProperty("Type", uint(gameType));
LogProperty("WagerData", uint(wagerData));
LogProperty("WageredWei", wager);
}
}