Skip to content

Commit ae7afbf

Browse files
committed
Improve hex encoder performance by factor 5
1 parent 853f13d commit ae7afbf

File tree

3 files changed

+189
-11
lines changed

3 files changed

+189
-11
lines changed

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## v1.2.0
44

55
* let hex decoder accept odd length string #37
6+
* improve hex encoder performance by factor 5
67

78
## v1.1.0
89

src/main/java/at/favre/lib/bytes/BinaryToTextEncoding.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ interface EncoderDecoder extends Encoder, Decoder {
7272
* Hex or Base16
7373
*/
7474
class Hex implements EncoderDecoder {
75+
private static final char[] LOOKUP_TABLE_LOWER = new char[]{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66};
76+
private static final char[] LOOKUP_TABLE_UPPER = new char[]{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46};
7577
private final boolean upperCase;
7678

7779
public Hex() {
@@ -84,22 +86,18 @@ public Hex(boolean upperCase) {
8486

8587
@Override
8688
public String encode(byte[] byteArray, ByteOrder byteOrder) {
87-
StringBuilder sb = new StringBuilder(byteArray.length * 2);
89+
90+
final char[] buffer = new char[byteArray.length * 2];
91+
final char[] lookup = upperCase ? LOOKUP_TABLE_UPPER : LOOKUP_TABLE_LOWER;
8892

8993
int index;
90-
char first4Bit;
91-
char last4Bit;
9294
for (int i = 0; i < byteArray.length; i++) {
9395
index = (byteOrder == ByteOrder.BIG_ENDIAN) ? i : byteArray.length - i - 1;
94-
first4Bit = Character.forDigit((byteArray[index] >> 4) & 0xF, 16);
95-
last4Bit = Character.forDigit((byteArray[index] & 0xF), 16);
96-
if (upperCase) {
97-
first4Bit = Character.toUpperCase(first4Bit);
98-
last4Bit = Character.toUpperCase(last4Bit);
99-
}
100-
sb.append(first4Bit).append(last4Bit);
96+
97+
buffer[i << 1] = lookup[(byteArray[index] >> 4) & 0xF];
98+
buffer[(i << 1) + 1] = lookup[(byteArray[index] & 0xF)];
10199
}
102-
return sb.toString();
100+
return new String(buffer);
103101
}
104102

105103
@Override
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* Copyright 2018 Patrick Favre-Bulle
3+
*
4+
* Licensed to the Apache Software Foundation (ASF) under one
5+
* or more contributor license agreements. See the NOTICE file
6+
* distributed with this work for additional information
7+
* regarding copyright ownership. The ASF licenses this file
8+
* to you under the Apache License, Version 2.0 (the
9+
* "License"); you may not use this file except in compliance
10+
* with the License. You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing,
15+
* software distributed under the License is distributed on an
16+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
* KIND, either express or implied. See the License for the
18+
* specific language governing permissions and limitations
19+
* under the License.
20+
*/
21+
22+
package at.favre.lib.bytes;
23+
24+
import org.openjdk.jmh.annotations.*;
25+
26+
import java.math.BigInteger;
27+
import java.nio.ByteOrder;
28+
import java.util.HashMap;
29+
import java.util.Map;
30+
import java.util.Random;
31+
import java.util.concurrent.TimeUnit;
32+
33+
/**
34+
* Simple benchmark checking the performance of hex encoding
35+
* <p>
36+
* Benchmark (byteLength) Mode Cnt Score Error Units
37+
* EncodingHexJmhBenchmark.encodeBigInteger 4 thrpt 4 5675746,504 ± 842567,828 ops/s
38+
* EncodingHexJmhBenchmark.encodeBigInteger 8 thrpt 4 1696726,355 ± 212646,110 ops/s
39+
* EncodingHexJmhBenchmark.encodeBigInteger 16 thrpt 4 880997,077 ± 116768,783 ops/s
40+
* EncodingHexJmhBenchmark.encodeBigInteger 128 thrpt 4 81326,528 ± 13169,476 ops/s
41+
* EncodingHexJmhBenchmark.encodeBigInteger 512 thrpt 4 15520,869 ± 3587,318 ops/s
42+
* EncodingHexJmhBenchmark.encodeBigInteger 1000000 thrpt 4 2,470 ± 0,110 ops/s
43+
* EncodingHexJmhBenchmark.encodeBytesLib 4 thrpt 4 15544365,475 ± 1333444,750 ops/s
44+
* EncodingHexJmhBenchmark.encodeBytesLib 8 thrpt 4 14342273,380 ± 1997502,302 ops/s
45+
* EncodingHexJmhBenchmark.encodeBytesLib 16 thrpt 4 12410491,100 ± 1671309,859 ops/s
46+
* EncodingHexJmhBenchmark.encodeBytesLib 128 thrpt 4 3896074,682 ± 453096,190 ops/s
47+
* EncodingHexJmhBenchmark.encodeBytesLib 512 thrpt 4 909938,189 ± 137965,178 ops/s
48+
* EncodingHexJmhBenchmark.encodeBytesLib 1000000 thrpt 4 465,305 ± 182,300 ops/s
49+
* EncodingHexJmhBenchmark.encodeStackOverflowCode 4 thrpt 4 15917799,229 ± 1133947,331 ops/s
50+
* EncodingHexJmhBenchmark.encodeStackOverflowCode 8 thrpt 4 14490924,588 ± 819188,772 ops/s
51+
* EncodingHexJmhBenchmark.encodeStackOverflowCode 16 thrpt 4 12635881,815 ± 1545063,635 ops/s
52+
* EncodingHexJmhBenchmark.encodeStackOverflowCode 128 thrpt 4 3807810,524 ± 499109,818 ops/s
53+
* EncodingHexJmhBenchmark.encodeStackOverflowCode 512 thrpt 4 917914,259 ± 122729,496 ops/s
54+
* EncodingHexJmhBenchmark.encodeStackOverflowCode 1000000 thrpt 4 471,778 ± 126,385 ops/s
55+
*/
56+
@State(Scope.Thread)
57+
@Fork(1)
58+
@Warmup(iterations = 2, time = 2)
59+
@Measurement(iterations = 4, time = 5)
60+
@BenchmarkMode(Mode.Throughput)
61+
@OutputTimeUnit(TimeUnit.SECONDS)
62+
public class EncodingHexJmhBenchmark {
63+
64+
@Param({"4", "8", "16", "128", "512", "1000000"})
65+
private int byteLength;
66+
private Map<Integer, Bytes[]> rndMap;
67+
68+
private BinaryToTextEncoding.EncoderDecoder option1;
69+
private BinaryToTextEncoding.EncoderDecoder option2;
70+
private BinaryToTextEncoding.EncoderDecoder option3;
71+
private BinaryToTextEncoding.EncoderDecoder option4;
72+
private Random random;
73+
74+
@Setup(Level.Trial)
75+
public void setup() {
76+
random = new Random();
77+
78+
option1 = new StackOverflowAnswer1Encoder();
79+
option2 = new BinaryToTextEncoding.Hex(true);
80+
option3 = new BigIntegerHexEncoder();
81+
option4 = new OldBytesImplementation();
82+
83+
rndMap = new HashMap<>();
84+
int[] lengths = new int[]{4, 8, 16, 128, 512, 1000000};
85+
for (int length : lengths) {
86+
int count = 10;
87+
rndMap.put(length, new Bytes[count]);
88+
for (int i = 0; i < count; i++) {
89+
rndMap.get(length)[i] = Bytes.random(length);
90+
}
91+
}
92+
}
93+
94+
@Benchmark
95+
public String encodeStackOverflowCode() {
96+
return encodeDecode(option1);
97+
}
98+
99+
@Benchmark
100+
public String encodeBytesLib() {
101+
return encodeDecode(option2);
102+
}
103+
104+
@Benchmark
105+
public String encodeBigInteger() {
106+
return encodeDecode(option3);
107+
}
108+
109+
@Benchmark
110+
public String encodeOldBytesLib() {
111+
return encodeDecode(option4);
112+
}
113+
114+
115+
private String encodeDecode(BinaryToTextEncoding.EncoderDecoder encoder) {
116+
Bytes[] bytes = rndMap.get(byteLength);
117+
int rndNum = random.nextInt(bytes.length);
118+
return encoder.encode(bytes[rndNum].array(), ByteOrder.BIG_ENDIAN);
119+
}
120+
121+
/**
122+
* See: https://stackoverflow.com/a/9855338/774398
123+
*/
124+
static final class StackOverflowAnswer1Encoder implements BinaryToTextEncoding.EncoderDecoder {
125+
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
126+
127+
@Override
128+
public String encode(byte[] bytes, ByteOrder byteOrder) {
129+
char[] hexChars = new char[bytes.length * 2];
130+
for (int j = 0; j < bytes.length; j++) {
131+
int v = bytes[j] & 0xFF;
132+
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
133+
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
134+
}
135+
return new String(hexChars);
136+
}
137+
138+
@Override
139+
public byte[] decode(CharSequence encoded) {
140+
throw new UnsupportedOperationException();
141+
}
142+
}
143+
144+
static final class BigIntegerHexEncoder implements BinaryToTextEncoding.EncoderDecoder {
145+
@Override
146+
public String encode(byte[] bytes, ByteOrder byteOrder) {
147+
return new BigInteger(1, bytes).toString(16);
148+
}
149+
150+
@Override
151+
public byte[] decode(CharSequence encoded) {
152+
throw new UnsupportedOperationException();
153+
}
154+
}
155+
156+
static final class OldBytesImplementation implements BinaryToTextEncoding.EncoderDecoder {
157+
158+
@Override
159+
public String encode(byte[] byteArray, ByteOrder byteOrder) {
160+
StringBuilder sb = new StringBuilder(byteArray.length * 2);
161+
162+
int index;
163+
char first4Bit;
164+
char last4Bit;
165+
for (int i = 0; i < byteArray.length; i++) {
166+
index = (byteOrder == ByteOrder.BIG_ENDIAN) ? i : byteArray.length - i - 1;
167+
first4Bit = Character.forDigit((byteArray[index] >> 4) & 0xF, 16);
168+
last4Bit = Character.forDigit((byteArray[index] & 0xF), 16);
169+
sb.append(first4Bit).append(last4Bit);
170+
}
171+
return sb.toString();
172+
}
173+
174+
@Override
175+
public byte[] decode(CharSequence encoded) {
176+
throw new UnsupportedOperationException();
177+
}
178+
}
179+
}

0 commit comments

Comments
 (0)