Abstract
In looking at deployed smart contracts on ethereum, 12%-16%+ of all generated solidity bytecode is masking operations. This is because the stack is 32 bytes wide, and safely using values smaller than that from untrusted or from packed sources require a cleaning operation.

Improvements here would be impactful, since bytecode size limits are a critical constraint for product development and working around them adds a lot of complication to the work of major protocols. Bytecode size also directly affects deployment costs.
Possible Methods
Here's a survey of different methods. I'll use masking addresses as examples, since they are the most common masking size.
Push
PUSH20 AND 22 bytes, 6 gas
This is the default output when the optimizer is off. It is gas optimal but it uses increasing amounts of bytecode as the mask gets bigger.
SHL/SUB
PUSH1 PUSH1 SHL PUSH1 SUB AND 9 bytes, 18 gas
This is an alternative used selectively by solidity with the optimizer on. This pushs a single bit, shifts that bit left to one bit above the mask target size, and then subtracts one to fill all bits below with ones and create the mask.
# push 1
0x000000000000000001
# <-- shift left
0x000001000000000000
# sub1
0x000000FFFFFFFFFFFF
This is not Pareto optimal - there are other ways that use both less gas and less bytes.
SHL/SHR
PUSH1 SHL PUSH1 SHR 6 bytes, 12 gas
This works by shifting left, to remove bytes to the left, then shifting right again to return the kept bits to their original position.
0x123456aaaaaaaaaaaa
# <-- shift left
0xaaaaaaaaaaaa000000
# --> shift rightt
0x000000aaaaaaaaaaaa
While it is more gas efficient than the next method, it does not easily compose with the existing optimization pipeline in solidity.
Negated(0) and SHIFT
ZERO NOT PUSH1 SHR AND 6 bytes , 15 gas
There is a stalled PR at #15935 that implements this method. There is also a rework of the same approach applied to a recent commit at #16729
MLOAD
PUSH1 MLOAD AND 4 bytes, 9 gas
By adding a 32 byte slot of ones after the solidity zero address, and moving where the solidity memory pointer points to, extremely efficient constants and masks can be created by just reading from the correct memory location.
The default memory now looks like:
SCRATCH
SCRATCH
FREEPOINTER
ZEROS
ONES
Then constants can be cheaply created by loading a memory region:
000000000000FFFFFFFFFFFF
|----------|
|----------|
A concept PR for this has been created #16708. This shows improvements of about 5% of total contact bytecode size vs the current SHL/SUB style size optimization, as well as corresponding gas improvements.
This method requires that the zeros and ones memory regions be untouched.
It also imposes a slight overhead of 5 bytes, and 15 gas. The bytecode overhead is more than paid back by the space savings throughout the rest of the contract. The gas overhead may result in a small additional gas usage for minimal contract methods that do not do masking.
Masking opcode
PUSH1 MASKBITS 3 bytes, 6 gas
The EVM could support a custom opcode for masking. I've written a draft EIP for a MASKBITS opcode that would be Pareto optimal choice for all but the smallest masks if implemented. The primary concern with this approach is if any code alters the reserved memory regions, then the compiled code will no longer behave as expected.
Abstract
In looking at deployed smart contracts on ethereum, 12%-16%+ of all generated solidity bytecode is masking operations. This is because the stack is 32 bytes wide, and safely using values smaller than that from untrusted or from packed sources require a cleaning operation.
Improvements here would be impactful, since bytecode size limits are a critical constraint for product development and working around them adds a lot of complication to the work of major protocols. Bytecode size also directly affects deployment costs.
Possible Methods
Here's a survey of different methods. I'll use masking addresses as examples, since they are the most common masking size.
Push
PUSH20AND22 bytes, 6 gasThis is the default output when the optimizer is off. It is gas optimal but it uses increasing amounts of bytecode as the mask gets bigger.
SHL/SUB
PUSH1PUSH1SHLPUSH1SUBAND9 bytes, 18 gasThis is an alternative used selectively by solidity with the optimizer on. This pushs a single bit, shifts that bit left to one bit above the mask target size, and then subtracts one to fill all bits below with ones and create the mask.
This is not Pareto optimal - there are other ways that use both less gas and less bytes.
SHL/SHR
PUSH1SHLPUSH1SHR6 bytes, 12 gasThis works by shifting left, to remove bytes to the left, then shifting right again to return the kept bits to their original position.
While it is more gas efficient than the next method, it does not easily compose with the existing optimization pipeline in solidity.
Negated(0) and SHIFT
ZERONOTPUSH1SHRAND6 bytes , 15 gasThere is a stalled PR at #15935 that implements this method. There is also a rework of the same approach applied to a recent commit at #16729
MLOAD
PUSH1MLOADAND4 bytes, 9 gasBy adding a 32 byte slot of ones after the solidity zero address, and moving where the solidity memory pointer points to, extremely efficient constants and masks can be created by just reading from the correct memory location.
The default memory now looks like:
Then constants can be cheaply created by loading a memory region:
A concept PR for this has been created #16708. This shows improvements of about 5% of total contact bytecode size vs the current SHL/SUB style size optimization, as well as corresponding gas improvements.
This method requires that the zeros and ones memory regions be untouched.
It also imposes a slight overhead of 5 bytes, and 15 gas. The bytecode overhead is more than paid back by the space savings throughout the rest of the contract. The gas overhead may result in a small additional gas usage for minimal contract methods that do not do masking.
Masking opcode
PUSH1MASKBITS3 bytes, 6 gasThe EVM could support a custom opcode for masking. I've written a draft EIP for a
MASKBITSopcode that would be Pareto optimal choice for all but the smallest masks if implemented. The primary concern with this approach is if any code alters the reserved memory regions, then the compiled code will no longer behave as expected.