Optimizations in NetUtil

Motivation:

IPv4/6 validation methods use allocations, which can be avoided.
IPv4 parse method use StringTokenizer.

Modifications:

Rewriting IPv4/6 validation methods to avoid allocations.
Rewriting IPv4 parse method without use StringTokenizer.

Result:

IPv4/6 validation and IPv4 parsing faster up to 2-10x.
This commit is contained in:
Nikolay Fedorovskikh 2017-05-19 00:14:01 +05:00 committed by Scott Mitchell
parent 0f1a2ca5ae
commit e4531918a3
5 changed files with 507 additions and 240 deletions

View File

@ -39,7 +39,6 @@ import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;
/**
* A class that holds a number of network-related constants.
@ -107,11 +106,6 @@ public final class NetUtil {
*/
private static final int IPV6_MAX_SEPARATORS = 8;
/**
* Number of bytes needed to represent and IPV4 value
*/
private static final int IPV4_BYTE_COUNT = 4;
/**
* Maximum amount of value adding characters in between IPV4 separators
*/
@ -369,17 +363,7 @@ public final class NetUtil {
public static byte[] createByteArrayFromIpAddressString(String ipAddressString) {
if (isValidIpV4Address(ipAddressString)) {
StringTokenizer tokenizer = new StringTokenizer(ipAddressString, ".");
String token;
int tempInt;
byte[] byteAddress = new byte[IPV4_BYTE_COUNT];
for (int i = 0; i < IPV4_BYTE_COUNT; i ++) {
token = tokenizer.nextToken();
tempInt = Integer.parseInt(token);
byteAddress[i] = (byte) tempInt;
}
return byteAddress;
return validIpV4ToBytes(ipAddressString);
}
if (isValidIpV6Address(ipAddressString)) {
@ -397,46 +381,52 @@ public final class NetUtil {
return null;
}
/**
* Convert ASCII hexadecimal character to the {@code int} value.
* Unlike {@link Character#digit(char, int)}, returns {@code 0} if character is not a HEX-represented.
*/
private static int getIntValue(char c) {
switch (c) {
case '0':
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'A' && c <= 'F') {
// 0xA - a start value in sequence 'A'..'F'
return c - 'A' + 0xA;
}
if (c >= 'a' && c <= 'f') {
// 0xA - a start value in sequence 'a'..'f'
return c - 'a' + 0xA;
}
return 0;
case '1':
return 1;
case '2':
return 2;
case '3':
return 3;
case '4':
return 4;
case '5':
return 5;
case '6':
return 6;
case '7':
return 7;
case '8':
return 8;
case '9':
return 9;
}
c = Character.toLowerCase(c);
switch (c) {
case 'a':
return 10;
case 'b':
return 11;
case 'c':
return 12;
case 'd':
return 13;
case 'e':
return 14;
case 'f':
return 15;
private static int decimalDigit(String str, int pos) {
return str.charAt(pos) - '0';
}
return 0;
private static byte ipv4WordToByte(String ip, int from, int toExclusive) {
int ret = decimalDigit(ip, from);
from++;
if (from == toExclusive) {
return (byte) ret;
}
ret = ret * 10 + decimalDigit(ip, from);
from++;
if (from == toExclusive) {
return (byte) ret;
}
return (byte) (ret * 10 + decimalDigit(ip, from));
}
// visible for tests
static byte[] validIpV4ToBytes(String ip) {
int i;
return new byte[] {
ipv4WordToByte(ip, 0, i = ip.indexOf('.', 1)),
ipv4WordToByte(ip, i + 1, i = ip.indexOf('.', i + 2)),
ipv4WordToByte(ip, i + 1, i = ip.indexOf('.', i + 2)),
ipv4WordToByte(ip, i + 1, ip.length())
};
}
/**
@ -489,167 +479,139 @@ public final class NetUtil {
}
}
public static boolean isValidIpV6Address(String ipAddress) {
boolean doubleColon = false;
int numberOfColons = 0;
int numberOfPeriods = 0;
StringBuilder word = new StringBuilder();
char c = 0;
char prevChar;
int startOffset = 0; // offset for [] ip addresses
int endOffset = ipAddress.length();
if (endOffset < 2) {
public static boolean isValidIpV6Address(String ip) {
int end = ip.length();
if (end < 2) {
return false;
}
// Strip []
if (ipAddress.charAt(0) == '[') {
if (ipAddress.charAt(endOffset - 1) != ']') {
return false; // must have a close ]
// strip "[]"
int start;
char c = ip.charAt(0);
if (c == '[') {
end--;
if (ip.charAt(end) != ']') {
// must have a close ]
return false;
}
start = 1;
c = ip.charAt(1);
} else {
start = 0;
}
startOffset = 1;
endOffset --;
int colons;
int compressBegin;
if (c == ':') {
// an IPv6 address can start with "::" or with a number
if (ip.charAt(start + 1) != ':') {
return false;
}
colons = 2;
compressBegin = start;
start += 2;
} else {
colons = 0;
compressBegin = -1;
}
// Strip the interface name/index after the percent sign.
int percentIdx = ipAddress.indexOf('%', startOffset);
if (percentIdx >= 0) {
endOffset = percentIdx;
int wordLen = 0;
loop:
for (int i = start; i < end; i++) {
c = ip.charAt(i);
if (isValidHexChar(c)) {
if (wordLen < 4) {
wordLen++;
continue;
}
return false;
}
for (int i = startOffset; i < endOffset; i ++) {
prevChar = c;
c = ipAddress.charAt(i);
switch (c) {
// case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d
case '.':
numberOfPeriods ++;
if (numberOfPeriods > 3) {
case ':':
if (colons > 7) {
return false;
} else if (numberOfPeriods == 1) {
}
if (ip.charAt(i - 1) == ':') {
if (compressBegin >= 0) {
return false;
}
compressBegin = i - 1;
} else {
wordLen = 0;
}
colons++;
break;
case '.':
// case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d
// check a normal case (6 single colons)
if (compressBegin < 0 && colons != 6 ||
// a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an
// IPv4 ending, otherwise 7 :'s is bad
(colons == 7 && compressBegin >= start || colons > 7)) {
return false;
}
// Verify this address is of the correct structure to contain an IPv4 address.
// It must be IPv4-Mapped or IPv4-Compatible
// (see https://tools.ietf.org/html/rfc4291#section-2.5.5).
int j = i - word.length() - 2; // index of character before the previous ':'.
final int beginColonIndex = ipAddress.lastIndexOf(':', j);
if (beginColonIndex == -1) {
return false;
}
char tmpChar = ipAddress.charAt(j);
if (isValidIPv4MappedChar(tmpChar)) {
if (j - beginColonIndex != 4 ||
!isValidIPv4MappedChar(ipAddress.charAt(j - 1)) ||
!isValidIPv4MappedChar(ipAddress.charAt(j - 2)) ||
!isValidIPv4MappedChar(ipAddress.charAt(j - 3))) {
int ipv4Start = i - wordLen;
int j = ipv4Start - 2; // index of character before the previous ':'.
if (isValidIPv4MappedChar(ip.charAt(j))) {
if (!isValidIPv4MappedChar(ip.charAt(j - 1)) ||
!isValidIPv4MappedChar(ip.charAt(j - 2)) ||
!isValidIPv4MappedChar(ip.charAt(j - 3))) {
return false;
}
j -= 5;
} else if (tmpChar == '0' || tmpChar == ':') {
--j;
} else {
return false;
}
// a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an
// IPv4 ending, otherwise 7 :'s is bad
if ((numberOfColons != 6 && !doubleColon) || numberOfColons > 7 ||
(numberOfColons == 7 && (ipAddress.charAt(startOffset) != ':' ||
ipAddress.charAt(1 + startOffset) != ':'))) {
return false;
}
for (; j >= startOffset; --j) {
tmpChar = ipAddress.charAt(j);
for (; j >= start; --j) {
char tmpChar = ip.charAt(j);
if (tmpChar != '0' && tmpChar != ':') {
return false;
}
}
}
if (!isValidIp4Word(word.toString())) {
return false;
// 7 - is minimum IPv4 address length
int ipv4End = ip.indexOf('%', ipv4Start + 7);
if (ipv4End < 0) {
ipv4End = end;
}
word.delete(0, word.length());
break;
case ':':
// FIX "IP6 mechanism syntax #ip6-bad1"
// An IPV6 address cannot start with a single ":".
// Either it can starti with "::" or with a number.
if (i == startOffset && (endOffset <= i || ipAddress.charAt(i + 1) != ':')) {
return false;
}
// END FIX "IP6 mechanism syntax #ip6-bad1"
numberOfColons ++;
if (numberOfColons > 8) {
return false;
}
if (numberOfPeriods > 0) {
return false;
}
if (prevChar == ':') {
if (doubleColon) {
return false;
}
doubleColon = true;
}
word.delete(0, word.length());
break;
return isValidIpV4Address(ip, ipv4Start, ipv4End);
case '%':
// strip the interface name/index after the percent sign
end = i;
break loop;
default:
if (word != null && word.length() > 3) {
return false;
}
if (!isValidHexChar(c)) {
return false;
}
word.append(c);
}
}
// Check if we have an IPv4 ending
if (numberOfPeriods > 0) {
// There is a test case with 7 colons and valid ipv4 this should resolve it
if (numberOfPeriods != 3 || !(isValidIp4Word(word.toString()) && (numberOfColons < 7 || doubleColon))) {
return false;
}
} else {
// If we're at then end and we haven't had 7 colons then there is a
// problem unless we encountered a doubleColon
if (numberOfColons != 7 && !doubleColon) {
return false;
}
if (word.length() == 0) {
// If we have an empty word at the end, it means we ended in either
// a : or a .
// If we did not end in :: then this is invalid
if (ipAddress.charAt(endOffset - 1) == ':' &&
ipAddress.charAt(endOffset - 2) != ':') {
return false;
}
} else if (numberOfColons == 8 && ipAddress.charAt(startOffset) != ':') {
return false;
}
}
return true;
// normal case without compression
if (compressBegin < 0) {
return colons == 7 && wordLen > 0;
}
private static boolean isValidIp4Word(String word) {
char c;
if (word.length() < 1 || word.length() > 3) {
return compressBegin + 2 == end ||
// 8 colons is valid only if compression in start or end
wordLen > 0 && (colons < 8 || compressBegin <= start);
}
private static boolean isValidIpV4Word(CharSequence word, int from, int toExclusive) {
int len = toExclusive - from;
char c0, c1, c2;
if (len < 1 || len > 3 || (c0 = word.charAt(from)) < '0') {
return false;
}
for (int i = 0; i < word.length(); i ++) {
c = word.charAt(i);
if (!(c >= '0' && c <= '9')) {
return false;
if (len == 3) {
return (c1 = word.charAt(from + 1)) >= '0' &&
(c2 = word.charAt(from + 2)) >= '0' &&
(c0 <= '1' && c1 <= '9' && c2 <= '9' ||
c0 == '2' && c1 <= '5' && (c2 <= '5' || c1 < '5' && c2 <= '9'));
}
}
return Integer.parseInt(word) <= 255;
return c0 <= '9' && (len == 1 || isValidNumericChar(word.charAt(from + 1)));
}
private static boolean isValidHexChar(char c) {
@ -684,46 +646,19 @@ public final class NetUtil {
* @return true, if the string represents an IPV4 address in dotted
* notation, false otherwise
*/
public static boolean isValidIpV4Address(String value) {
public static boolean isValidIpV4Address(String ip) {
return isValidIpV4Address(ip, 0, ip.length());
}
int periods = 0;
@SuppressWarnings("DuplicateBooleanBranch")
private static boolean isValidIpV4Address(String ip, int from, int toExcluded) {
int len = toExcluded - from;
int i;
int length = value.length();
if (length > 15) {
return false;
}
char c;
StringBuilder word = new StringBuilder();
for (i = 0; i < length; i ++) {
c = value.charAt(i);
if (c == '.') {
periods ++;
if (periods > 3) {
return false;
}
if (word.length() == 0) {
return false;
}
if (Integer.parseInt(word.toString()) > 255) {
return false;
}
word.delete(0, word.length());
} else if (!Character.isDigit(c)) {
return false;
} else {
if (word.length() > 2) {
return false;
}
word.append(c);
}
}
if (word.length() == 0 || Integer.parseInt(word.toString()) > 255) {
return false;
}
return periods == 3;
return len <= 15 && len >= 7 &&
(i = ip.indexOf('.', from + 1)) > 0 && isValidIpV4Word(ip, from, i) &&
(i = ip.indexOf('.', from = i + 2)) > 0 && isValidIpV4Word(ip, from - 1, i) &&
(i = ip.indexOf('.', from = i + 2)) > 0 && isValidIpV4Word(ip, from - 1, i) &&
isValidIpV4Word(ip, i + 1, toExcluded);
}
/**
@ -1074,7 +1009,7 @@ public final class NetUtil {
// Find longest run of 0s, tie goes to first found instance
int currentStart = -1;
int currentLength = 0;
int currentLength;
int shortestStart = -1;
int shortestLength = 0;
for (i = 0; i < words.length; ++i) {

View File

@ -52,6 +52,7 @@ public class NetUtilTest {
"172.18.5.4", "ac120504",
"0.0.0.0", "00000000",
"127.0.0.1", "7f000001",
"255.255.255.255", "ffffffff",
"1.2.3.4", "01020304");
private static final Map<String, String> invalidIpV4Hosts = new TestMap(
@ -69,6 +70,20 @@ public class NetUtilTest {
"19a.0.1.1", null,
"a.0.1.1", null,
".0.1.1", null,
"127.0.0", null,
"192.0.1.256", null,
"0.0.200.259", null,
"1.1.-1.1", null,
"1.1. 1.1", null,
"1.1.1.1 ", null,
"1.1.+1.1", null,
"0.0x1.0.255", null,
"0.01x.0.255", null,
"0.x01.0.255", null,
"0.-.0.0", null,
"0..0.0", null,
"0.A.0.0", null,
"0.1111.0.0", null,
"...", null);
private static final Map<String, String> validIpV6Hosts = new TestMap(
@ -687,6 +702,7 @@ public class NetUtilTest {
public void testBytesToIpAddress() throws UnknownHostException {
for (Entry<String, String> e : validIpV4Hosts.entrySet()) {
assertEquals(e.getKey(), bytesToIpAddress(createByteArrayFromIpAddressString(e.getKey())));
assertEquals(e.getKey(), bytesToIpAddress(validIpV4ToBytes(e.getKey())));
}
for (Entry<byte[], String> testEntry : ipv6ToAddressStrings.entrySet()) {
assertEquals(testEntry.getValue(), bytesToIpAddress(testEntry.getKey()));

View File

@ -32,7 +32,7 @@
<!-- Skip tests by default; run only if -DskipTests=false is specified -->
<skipTests>true</skipTests>
<epoll.arch>x86_64</epoll.arch>
<jmh.version>1.17.4</jmh.version>
<jmh.version>1.19</jmh.version>
</properties>
<profiles>

View File

@ -0,0 +1,91 @@
/*
* Copyright 2017 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.microbenchmark.common;
import io.netty.microbench.util.AbstractMicrobenchmark;
import io.netty.util.NetUtil;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import java.util.concurrent.TimeUnit;
@Threads(1)
@Warmup(iterations = 3)
@Measurement(iterations = 3)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class IsValidIpV4Benchmark extends AbstractMicrobenchmark {
@Param({ "127.0.0.1", "255.255.255.255", "1.1.1.1", "127.0.0.256", "127.0.0.1.1", "127.0.0", "[2001::1]" })
private String ip;
public static boolean isValidIpV4AddressOld(String value) {
int periods = 0;
int i;
int length = value.length();
if (length > 15) {
return false;
}
char c;
StringBuilder word = new StringBuilder();
for (i = 0; i < length; i++) {
c = value.charAt(i);
if (c == '.') {
periods++;
if (periods > 3) {
return false;
}
if (word.length() == 0) {
return false;
}
if (Integer.parseInt(word.toString()) > 255) {
return false;
}
word.delete(0, word.length());
} else if (!Character.isDigit(c)) {
return false;
} else {
if (word.length() > 2) {
return false;
}
word.append(c);
}
}
if (word.length() == 0 || Integer.parseInt(word.toString()) > 255) {
return false;
}
return periods == 3;
}
// Tests
@Benchmark
public boolean isValidIpV4AddressOld() {
return isValidIpV4AddressOld(ip);
}
@Benchmark
public boolean isValidIpV4AddressNew() {
return NetUtil.isValidIpV4Address(ip);
}
}

View File

@ -0,0 +1,225 @@
/*
* Copyright 2017 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.microbenchmark.common;
import io.netty.microbench.util.AbstractMicrobenchmark;
import io.netty.util.NetUtil;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import java.util.concurrent.TimeUnit;
@Threads(1)
@Warmup(iterations = 3)
@Measurement(iterations = 3)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class IsValidIpV6Benchmark extends AbstractMicrobenchmark {
@Param({
"127.0.0.1", "fdf8:f53b:82e4::53", "2001::1",
"2001:0000:4136:e378:8000:63bf:3fff:fdd2", "0:0:0:0:0:0:10.0.0.1"
})
private String ip;
private static boolean isValidIp4Word(String word) {
char c;
if (word.length() < 1 || word.length() > 3) {
return false;
}
for (int i = 0; i < word.length(); i++) {
c = word.charAt(i);
if (!(c >= '0' && c <= '9')) {
return false;
}
}
return Integer.parseInt(word) <= 255;
}
private static boolean isValidHexChar(char c) {
return c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f';
}
private static boolean isValidIPv4MappedChar(char c) {
return c == 'f' || c == 'F';
}
public static boolean isValidIpV6AddressOld(String ipAddress) {
boolean doubleColon = false;
int numberOfColons = 0;
int numberOfPeriods = 0;
StringBuilder word = new StringBuilder();
char c = 0;
char prevChar;
int startOffset = 0; // offset for [] ip addresses
int endOffset = ipAddress.length();
if (endOffset < 2) {
return false;
}
// Strip []
if (ipAddress.charAt(0) == '[') {
if (ipAddress.charAt(endOffset - 1) != ']') {
return false; // must have a close ]
}
startOffset = 1;
endOffset--;
}
// Strip the interface name/index after the percent sign.
int percentIdx = ipAddress.indexOf('%', startOffset);
if (percentIdx >= 0) {
endOffset = percentIdx;
}
for (int i = startOffset; i < endOffset; i++) {
prevChar = c;
c = ipAddress.charAt(i);
switch (c) {
// case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d
case '.':
numberOfPeriods++;
if (numberOfPeriods > 3) {
return false;
}
if (numberOfPeriods == 1) {
// Verify this address is of the correct structure to contain an IPv4 address.
// It must be IPv4-Mapped or IPv4-Compatible
// (see https://tools.ietf.org/html/rfc4291#section-2.5.5).
int j = i - word.length() - 2; // index of character before the previous ':'.
final int beginColonIndex = ipAddress.lastIndexOf(':', j);
if (beginColonIndex == -1) {
return false;
}
char tmpChar = ipAddress.charAt(j);
if (isValidIPv4MappedChar(tmpChar)) {
if (j - beginColonIndex != 4 ||
!isValidIPv4MappedChar(ipAddress.charAt(j - 1)) ||
!isValidIPv4MappedChar(ipAddress.charAt(j - 2)) ||
!isValidIPv4MappedChar(ipAddress.charAt(j - 3))) {
return false;
}
j -= 5;
} else if (tmpChar == '0' || tmpChar == ':') {
--j;
} else {
return false;
}
// a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an
// IPv4 ending, otherwise 7 :'s is bad
if ((numberOfColons != 6 && !doubleColon) || numberOfColons > 7 ||
(numberOfColons == 7 && (ipAddress.charAt(startOffset) != ':' ||
ipAddress.charAt(1 + startOffset) != ':'))) {
return false;
}
for (; j >= startOffset; --j) {
tmpChar = ipAddress.charAt(j);
if (tmpChar != '0' && tmpChar != ':') {
return false;
}
}
}
if (!isValidIp4Word(word.toString())) {
return false;
}
word.delete(0, word.length());
break;
case ':':
// FIX "IP6 mechanism syntax #ip6-bad1"
// An IPV6 address cannot start with a single ":".
// Either it can start with "::" or with a number.
if (i == startOffset && (endOffset <= i || ipAddress.charAt(i + 1) != ':')) {
return false;
}
// END FIX "IP6 mechanism syntax #ip6-bad1"
numberOfColons++;
if (numberOfColons > 8) {
return false;
}
if (numberOfPeriods > 0) {
return false;
}
if (prevChar == ':') {
if (doubleColon) {
return false;
}
doubleColon = true;
}
word.delete(0, word.length());
break;
default:
if (word != null && word.length() > 3) {
return false;
}
if (!isValidHexChar(c)) {
return false;
}
word.append(c);
}
}
// Check if we have an IPv4 ending
if (numberOfPeriods > 0) {
// There is a test case with 7 colons and valid ipv4 this should resolve it
if (numberOfPeriods != 3 ||
!(isValidIp4Word(word.toString()) && (numberOfColons < 7 || doubleColon))) {
return false;
}
} else {
// If we're at then end and we haven't had 7 colons then there is a
// problem unless we encountered a doubleColon
if (numberOfColons != 7 && !doubleColon) {
return false;
}
if (word.length() == 0) {
// If we have an empty word at the end, it means we ended in either
// a : or a .
// If we did not end in :: then this is invalid
if (ipAddress.charAt(endOffset - 1) == ':' &&
ipAddress.charAt(endOffset - 2) != ':') {
return false;
}
} else if (numberOfColons == 8 && ipAddress.charAt(startOffset) != ':') {
return false;
}
}
return true;
}
// Tests
@Benchmark
public boolean isValidIpV6AddressOld() {
return isValidIpV6AddressOld(ip);
}
@Benchmark
public boolean isValidIpV6AddressNew() {
return NetUtil.isValidIpV6Address(ip);
}
}