Skip to content

Conversation

Philogy
Copy link
Contributor

@Philogy Philogy commented Feb 5, 2025

Description

Optimized address to hex string with some fancy bit operations. Has a fairly large gas trade-off ~360 bytes more by the looks of it, open questions:

  • worth the codesize trade off?
  • document how it works?

Checklist

  • Ran forge fmt?
  • Ran forge test?

@Philogy
Copy link
Contributor Author

Philogy commented Feb 5, 2025

Also not sure what's wrong with the diff ci check, showing error:

fatal: a branch named 'main' already exists

@Vectorized
Copy link
Owner

What made you think about this trick?

@Philogy
Copy link
Contributor Author

Philogy commented Feb 5, 2025

I was benchmarking and trying to optimize address checksum computation for a project of mine. I was looking at a table of hex nibble to binary to ascii-binary table and noticed that you could relatively cheaply compute:

  • bitmap of whether each nibble will be a letter or not
  • and eventually how to make a padded nibble into it's ascii representation
'0' 0000 -> 00110000
'1' 0001 -> 00110001
'2' 0010 -> 00110010
'3' 0011 -> 00110011
'4' 0100 -> 00110100
'5' 0101 -> 00110101
'6' 0110 -> 00110110
'7' 0111 -> 00110111
'8' 1000 -> 00111000
'9' 1001 -> 00111001
'a' 1010 -> 01100001
'b' 1011 -> 01100010
'c' 1100 -> 01100011
'd' 1101 -> 01100100
'e' 1110 -> 01100101
'f' 1111 -> 01100110

I noticed that a-f all have bit 4 set, but to exclude 8 & 9 you need to also check bit 2 & 3, giving you: b4 && (b3 || b2), which you can do by shifting and masking the word a couple times.

For the ascii representation I noticed it's basically:

          b8   b7   b6   b5   b4   b3   b2   b1
digit:     0    0    1    1   [       x       ]
letter:    0    1    1    0   [   8 ^ x - 1   ]

So yeah once you have the nibbles spaced out, padded and you have a bitmap of which byte will be a letter or not it's relatively easy to get the ascii representation.

Comment on lines +380 to +381
shl(64, and(value, 0xffffffffffffffff0000000000000000)),
and(value, 0xffffffffffffffff)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(1 << 64) - 1 = 0xffffffffffffffff

(1 << 64) - 1 << 64 = 0xffffffffffffffff0000000000000000

)
w :=
and(
0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff, or(shl(8, w), w)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0xff * (type(uint256).max / type(uint16).max) = 0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff

)
w :=
and(
0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0xffff * (type(uint256).max / type(uint32).max) = 0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff


w :=
and(
0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0xffffffff * (type(uint256).max / type(uint64).max) = 0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff

w :=
sub(
xor(
xor(w, 0x3030303030303030303030303030303030303030303030303030303030303030),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0x30 * (type(uint).max / 0xff) = 0x3030303030303030303030303030303030303030303030303030303030303030

@0xClandestine
Copy link
Contributor

0xClandestine commented Jul 13, 2025

Can cheaply create these masks JIT using the below formula:

function repeat(uint256 x, uint strideBits) internal pure returns (uint256) {
    return x * (type(uint256).max / ((1 << strideBits) - 1));
}

@0xClandestine
Copy link
Contributor

Can cheaply create these masks JIT using the below formula:

function repeat(uint256 x, uint strideBits) internal pure returns (uint256) {
    return x * (type(uint256).max / ((1 << strideBits) - 1));
}

Using this method to generate masks saved about 50 bytes of codesize, at the expense of additional gas likely not worth it but interesting way to generate repeat constants nonetheless.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants