Skip to content

Commit bdcb5b4

Browse files
authored
Optimise toFastHex for engine_getBlobsV2 (#9426)
- Avoids Tuweni's megamorphic get() and size() calls - Add jmh ToFastHexBenchmark Signed-off-by: Simon Dudley <[email protected]>
1 parent 4439d04 commit bdcb5b4

File tree

5 files changed

+252
-2
lines changed

5 files changed

+252
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- Use error code 3 for execution reverted [#9365](https://github.com/hyperledger/besu/pull/9365)
2626
- eth_createAccessList now returns success result if execution reverted [#9358](https://github.com/hyperledger/besu/pull/9358)
2727
- Use Eclipse Temurin OpenJDK JRE in Besu docker image [#9392](https://github.com/hyperledger/besu/pull/9392)
28+
- Performance: 5-6x faster toFastHex calculation for engine_getBlobsV2 [#9426](https://github.com/hyperledger/besu/pull/9426)
2829

2930
### Bug fixes
3031
- Fix loss of colored output in terminal when using `--color-enabled=true` option [#8908](https://github.com/hyperledger/besu/issues/8908)

ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetBlobsV2.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.hyperledger.besu.metrics.BesuMetricCategory;
3737
import org.hyperledger.besu.plugin.services.MetricsSystem;
3838
import org.hyperledger.besu.plugin.services.metrics.Counter;
39+
import org.hyperledger.besu.util.HexUtils;
3940

4041
import java.util.ArrayList;
4142
import java.util.List;
@@ -164,9 +165,9 @@ private VersionedHash[] extractVersionedHashes(final JsonRpcRequestContext reque
164165

165166
private BlobAndProofV2 createBlobAndProofV2(final BlobProofBundle blobProofBundle) {
166167
return new BlobAndProofV2(
167-
blobProofBundle.getBlob().getData().toHexString(),
168+
HexUtils.toFastHex(blobProofBundle.getBlob().getData(), true),
168169
blobProofBundle.getKzgProof().stream()
169-
.map(proof -> proof.getData().toHexString())
170+
.map(proof -> HexUtils.toFastHex(proof.getData(), true))
170171
.toList());
171172
}
172173

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright contributors to Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.ethereum.vm.operations;
16+
17+
import org.hyperledger.besu.util.HexUtils;
18+
19+
import java.nio.ByteBuffer;
20+
import java.util.Random;
21+
import java.util.concurrent.TimeUnit;
22+
23+
import org.apache.tuweni.bytes.Bytes;
24+
import org.openjdk.jmh.annotations.Benchmark;
25+
import org.openjdk.jmh.annotations.BenchmarkMode;
26+
import org.openjdk.jmh.annotations.Measurement;
27+
import org.openjdk.jmh.annotations.Mode;
28+
import org.openjdk.jmh.annotations.OperationsPerInvocation;
29+
import org.openjdk.jmh.annotations.OutputTimeUnit;
30+
import org.openjdk.jmh.annotations.Param;
31+
import org.openjdk.jmh.annotations.Scope;
32+
import org.openjdk.jmh.annotations.Setup;
33+
import org.openjdk.jmh.annotations.State;
34+
import org.openjdk.jmh.annotations.Warmup;
35+
import org.openjdk.jmh.infra.Blackhole;
36+
37+
@State(Scope.Thread)
38+
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
39+
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
40+
@OutputTimeUnit(value = TimeUnit.NANOSECONDS)
41+
@BenchmarkMode(Mode.AverageTime)
42+
public class ToFastHexBenchmark {
43+
44+
// engine_getBlobsV2 worse case sizes:
45+
// 1 blob = 128 KB = 131072 bytes + (1 * 128 proofs * 48 bytes) = 137,216 bytes
46+
// 9 blobs = 9 * 131072 = 1,179,648 bytes + (9 * 128 proofs * 48 bytes) = 1,234,944 bytes (~1.2
47+
// MB)
48+
// 21 blobs = 21 * 131072 = 2,752,512 bytes + (21 * 128 proofs * 48 bytes) = 2,881,536 bytes (~2.7
49+
// MB)
50+
// 72 blobs = 72 * 131072 = 9,437,184 bytes + (72 * 128 proofs * 48 bytes) = 9,879,552 bytes (~9.4
51+
// MB)
52+
// 128 blobs = 128 * 131072 = 16,777,216 bytes + (128 * 128 proofs * 48 bytes) = 17,563,648 bytes
53+
// (~16.7 MB)
54+
public enum Case {
55+
SIZE_1_BLOB(137_216),
56+
SIZE_9_BLOBS(1_234_944),
57+
SIZE_21_BLOBS(2_881_536),
58+
SIZE_72_BLOBS(9_879_552),
59+
SIZE_128_BLOBS(17_563_648);
60+
61+
final int inputSize;
62+
63+
Case(final int inputSize) {
64+
this.inputSize = inputSize;
65+
}
66+
}
67+
68+
@Param({"SIZE_1_BLOB", "SIZE_9_BLOBS", "SIZE_21_BLOBS", "SIZE_72_BLOBS", "SIZE_128_BLOBS"})
69+
private Case caseName;
70+
71+
private static final int N = 3;
72+
private static final int FACTOR = 1;
73+
private static final Random RANDOM = new Random(23L);
74+
Bytes[] bytes;
75+
76+
@Setup
77+
public void setup() {
78+
// force megamorphic tuweni behavior by mixing different Bytes implementations
79+
bytes = new Bytes[N * FACTOR];
80+
for (int i = 0; i < N * FACTOR; i += N) {
81+
bytes[i] = Bytes.wrap(getBytes(caseName.inputSize));
82+
bytes[i + 1] = Bytes.wrapByteBuffer(ByteBuffer.wrap(getBytes(caseName.inputSize)));
83+
bytes[i + 2] = Bytes.repeat((byte) 0x09, caseName.inputSize);
84+
}
85+
}
86+
87+
private static byte[] getBytes(final int size) {
88+
byte[] b = new byte[size];
89+
RANDOM.nextBytes(b);
90+
return b;
91+
}
92+
93+
@Benchmark
94+
@OperationsPerInvocation(N * FACTOR)
95+
public void hexUtils(final Blackhole blackhole) {
96+
for (Bytes b : bytes) {
97+
blackhole.consume(HexUtils.toFastHex(b, true));
98+
}
99+
}
100+
101+
@Benchmark
102+
@OperationsPerInvocation(N * FACTOR)
103+
public void tuweni(final Blackhole blackhole) {
104+
for (Bytes b : bytes) {
105+
blackhole.consume(b.toFastHex(true));
106+
}
107+
}
108+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright contributors to Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.util;
16+
17+
import org.apache.tuweni.bytes.Bytes;
18+
19+
/** Utility class for Hexadecimal operations. */
20+
public class HexUtils {
21+
22+
private HexUtils() {}
23+
24+
private static final char[] HEX = "0123456789abcdef".toCharArray();
25+
26+
/**
27+
* Optimized version of org.apache.tuweni.bytes.Bytes.toFastHex that avoids the megamorphic get
28+
* and size calls
29+
*
30+
* @param abytes The bytes to convert
31+
* @param prefix whether to include the "0x" prefix
32+
* @return The hex string representation
33+
*/
34+
public static String toFastHex(final Bytes abytes, final boolean prefix) {
35+
final byte[] bytes = abytes.toArrayUnsafe();
36+
final int size = bytes.length;
37+
38+
final int offset = prefix ? 2 : 0;
39+
40+
final int resultSize = (size * 2) + offset;
41+
42+
final char[] result = new char[resultSize];
43+
44+
if (prefix) {
45+
result[0] = '0';
46+
result[1] = 'x';
47+
}
48+
49+
for (int i = 0; i < size; i++) {
50+
byte b = bytes[i];
51+
int pos = i * 2;
52+
result[pos + offset] = HEX[b >> 4 & 15];
53+
result[pos + offset + 1] = HEX[b & 15];
54+
}
55+
56+
return new String(result);
57+
}
58+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright contributors to Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.util;
16+
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
19+
import org.apache.tuweni.bytes.Bytes;
20+
import org.junit.jupiter.api.Test;
21+
22+
class HexUtilsTest {
23+
24+
@Test
25+
public void testToFastHexEmptyWithPrefix() {
26+
Bytes emptyBytes = Bytes.EMPTY;
27+
String result = HexUtils.toFastHex(emptyBytes, true);
28+
assertEquals("0x", result, "Expected '0x' for an empty byte array with prefix");
29+
}
30+
31+
@Test
32+
public void testToFastHexEmptyWithoutPrefix() {
33+
Bytes emptyBytes = Bytes.EMPTY;
34+
String result = HexUtils.toFastHex(emptyBytes, false);
35+
assertEquals("", result, "Expected '' for an empty byte array without prefix");
36+
}
37+
38+
@Test
39+
public void testToFastHexSingleByteWithPrefix() {
40+
Bytes bytes = Bytes.fromHexString("0x01");
41+
String result = HexUtils.toFastHex(bytes, true);
42+
assertEquals("0x01", result, "Expected '0x01' for the byte 0x01 with prefix");
43+
}
44+
45+
@Test
46+
public void testToFastHexSingleByteWithoutPrefix() {
47+
Bytes bytes = Bytes.fromHexString("0x01");
48+
String result = HexUtils.toFastHex(bytes, false);
49+
assertEquals("01", result, "Expected '01' for the byte 0x01 without prefix");
50+
}
51+
52+
@Test
53+
public void testToFastHexMultipleBytesWithPrefix() {
54+
Bytes bytes = Bytes.fromHexString("0x010203");
55+
String result = HexUtils.toFastHex(bytes, true);
56+
assertEquals("0x010203", result, "Expected '0x010203' for the byte array 0x010203 with prefix");
57+
}
58+
59+
@Test
60+
public void testToFastHexMultipleBytesWithoutPrefix() {
61+
Bytes bytes = Bytes.fromHexString("0x010203");
62+
String result = HexUtils.toFastHex(bytes, false);
63+
assertEquals("010203", result, "Expected '010203' for the byte array 0x010203 without prefix");
64+
}
65+
66+
@Test
67+
public void testToFastHexWithLeadingZeros() {
68+
Bytes bytes = Bytes.fromHexString("0x0001");
69+
String result = HexUtils.toFastHex(bytes, true);
70+
assertEquals(
71+
"0x0001",
72+
result,
73+
"Expected '0x0001' for the byte array 0x0001 with prefix (leading zeros retained)");
74+
}
75+
76+
@Test
77+
public void testToFastHexWithLargeData() {
78+
Bytes bytes = Bytes.fromHexString("0x0102030405060708090a");
79+
String result = HexUtils.toFastHex(bytes, true);
80+
assertEquals("0x0102030405060708090a", result, "Expected correct hex output for large data");
81+
}
82+
}

0 commit comments

Comments
 (0)