Improve DateFormatter parsing performance (#8821)

Motivation:

Just was looking through code and found 1 interesting place DateFormatter.tryParseMonth that was not very effective, so I decided to optimize it a bit.

Modification:

Changed DateFormatter.tryParseMonth method. Instead of invocation regionMatch() for every month - compare chars one by one.

Result:

DateFormatter.parseHttpDate method performance improved from ~3% to ~15%.

Benchmark                                                                (DATE_STRING)   Mode  Cnt        Score       Error  Units
DateFormatter2Benchmark.parseHttpHeaderDateFormatter     Sun, 27 Jan 2016 19:18:46 GMT  thrpt    6  4142781.221 ± 82155.002  ops/s
DateFormatter2Benchmark.parseHttpHeaderDateFormatter     Sun, 27 Dec 2016 19:18:46 GMT  thrpt    6  3781810.558 ± 38679.061  ops/s
DateFormatter2Benchmark.parseHttpHeaderDateFormatterNew  Sun, 27 Jan 2016 19:18:46 GMT  thrpt    6  4372569.705 ± 30257.537  ops/s
DateFormatter2Benchmark.parseHttpHeaderDateFormatterNew  Sun, 27 Dec 2016 19:18:46 GMT  thrpt    6  4339785.100 ± 57542.660  ops/s
This commit is contained in:
Dmitriy Dumanskiy 2019-02-04 11:04:20 +02:00 committed by Norman Maurer
parent cfa60552fb
commit 2e433889b2
4 changed files with 141 additions and 17 deletions

View File

@ -261,10 +261,6 @@ public final class DateFormatter {
return false; return false;
} }
private static boolean matchMonth(String month, CharSequence txt, int tokenStart) {
return AsciiString.regionMatchesAscii(month, true, 0, txt, tokenStart, 3);
}
private boolean tryParseMonth(CharSequence txt, int tokenStart, int tokenEnd) { private boolean tryParseMonth(CharSequence txt, int tokenStart, int tokenEnd) {
int len = tokenEnd - tokenStart; int len = tokenEnd - tokenStart;
@ -272,29 +268,33 @@ public final class DateFormatter {
return false; return false;
} }
if (matchMonth("Jan", txt, tokenStart)) { char monthChar1 = AsciiString.toLowerCase(txt.charAt(tokenStart));
char monthChar2 = AsciiString.toLowerCase(txt.charAt(tokenStart + 1));
char monthChar3 = AsciiString.toLowerCase(txt.charAt(tokenStart + 2));
if (monthChar1 == 'j' && monthChar2 == 'a' && monthChar3 == 'n') {
month = Calendar.JANUARY; month = Calendar.JANUARY;
} else if (matchMonth("Feb", txt, tokenStart)) { } else if (monthChar1 == 'f' && monthChar2 == 'e' && monthChar3 == 'b') {
month = Calendar.FEBRUARY; month = Calendar.FEBRUARY;
} else if (matchMonth("Mar", txt, tokenStart)) { } else if (monthChar1 == 'm' && monthChar2 == 'a' && monthChar3 == 'r') {
month = Calendar.MARCH; month = Calendar.MARCH;
} else if (matchMonth("Apr", txt, tokenStart)) { } else if (monthChar1 == 'a' && monthChar2 == 'p' && monthChar3 == 'r') {
month = Calendar.APRIL; month = Calendar.APRIL;
} else if (matchMonth("May", txt, tokenStart)) { } else if (monthChar1 == 'm' && monthChar2 == 'a' && monthChar3 == 'y') {
month = Calendar.MAY; month = Calendar.MAY;
} else if (matchMonth("Jun", txt, tokenStart)) { } else if (monthChar1 == 'j' && monthChar2 == 'u' && monthChar3 == 'n') {
month = Calendar.JUNE; month = Calendar.JUNE;
} else if (matchMonth("Jul", txt, tokenStart)) { } else if (monthChar1 == 'j' && monthChar2 == 'u' && monthChar3 == 'l') {
month = Calendar.JULY; month = Calendar.JULY;
} else if (matchMonth("Aug", txt, tokenStart)) { } else if (monthChar1 == 'a' && monthChar2 == 'u' && monthChar3 == 'g') {
month = Calendar.AUGUST; month = Calendar.AUGUST;
} else if (matchMonth("Sep", txt, tokenStart)) { } else if (monthChar1 == 's' && monthChar2 == 'e' && monthChar3 == 'p') {
month = Calendar.SEPTEMBER; month = Calendar.SEPTEMBER;
} else if (matchMonth("Oct", txt, tokenStart)) { } else if (monthChar1 == 'o' && monthChar2 == 'c' && monthChar3 == 't') {
month = Calendar.OCTOBER; month = Calendar.OCTOBER;
} else if (matchMonth("Nov", txt, tokenStart)) { } else if (monthChar1 == 'n' && monthChar2 == 'o' && monthChar3 == 'v') {
month = Calendar.NOVEMBER; month = Calendar.NOVEMBER;
} else if (matchMonth("Dec", txt, tokenStart)) { } else if (monthChar1 == 'd' && monthChar2 == 'e' && monthChar3 == 'c') {
month = Calendar.DECEMBER; month = Calendar.DECEMBER;
} else { } else {
return false; return false;

View File

@ -17,6 +17,7 @@ package io.netty.handler.codec;
import org.junit.Test; import org.junit.Test;
import java.util.Calendar;
import java.util.Date; import java.util.Date;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -111,4 +112,26 @@ public class DateFormatterTest {
public void testFormat() { public void testFormat() {
assertEquals("Sun, 6 Nov 1994 08:49:37 GMT", format(DATE)); assertEquals("Sun, 6 Nov 1994 08:49:37 GMT", format(DATE));
} }
@Test
public void testParseAllMonths() {
assertEquals(Calendar.JANUARY, getMonth(parseHttpDate("Sun, 6 Jan 1994 08:49:37 GMT")));
assertEquals(Calendar.FEBRUARY, getMonth(parseHttpDate("Sun, 6 Feb 1994 08:49:37 GMT")));
assertEquals(Calendar.MARCH, getMonth(parseHttpDate("Sun, 6 Mar 1994 08:49:37 GMT")));
assertEquals(Calendar.APRIL, getMonth(parseHttpDate("Sun, 6 Apr 1994 08:49:37 GMT")));
assertEquals(Calendar.MAY, getMonth(parseHttpDate("Sun, 6 May 1994 08:49:37 GMT")));
assertEquals(Calendar.JUNE, getMonth(parseHttpDate("Sun, 6 Jun 1994 08:49:37 GMT")));
assertEquals(Calendar.JULY, getMonth(parseHttpDate("Sun, 6 Jul 1994 08:49:37 GMT")));
assertEquals(Calendar.AUGUST, getMonth(parseHttpDate("Sun, 6 Aug 1994 08:49:37 GMT")));
assertEquals(Calendar.SEPTEMBER, getMonth(parseHttpDate("Sun, 6 Sep 1994 08:49:37 GMT")));
assertEquals(Calendar.OCTOBER, getMonth(parseHttpDate("Sun Oct 6 08:49:37 1994")));
assertEquals(Calendar.NOVEMBER, getMonth(parseHttpDate("Sun Nov 6 08:49:37 1994")));
assertEquals(Calendar.DECEMBER, getMonth(parseHttpDate("Sun Dec 6 08:49:37 1994")));
}
private static int getMonth(Date referenceDate) {
Calendar cal = Calendar.getInstance();
cal.setTime(referenceDate);
return cal.get(Calendar.MONTH);
}
} }

View File

@ -1827,7 +1827,13 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
return isUpperCase(b) ? (byte) (b + 32) : b; return isUpperCase(b) ? (byte) (b + 32) : b;
} }
private static char toLowerCase(char c) { /**
* If the character is uppercase - converts the character to lowercase,
* otherwise returns the character as it is. Only for ASCII characters.
*
* @return lowercase ASCII character equivalent
*/
public static char toLowerCase(char c) {
return isUpperCase(c) ? (char) (c + 32) : c; return isUpperCase(c) ? (char) (c + 32) : c;
} }

View File

@ -0,0 +1,95 @@
/*
* Copyright 2019 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.handler.codec;
import io.netty.microbench.util.AbstractMicrobenchmark;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import java.util.Date;
@Threads(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
public class DateFormatter2Benchmark extends AbstractMicrobenchmark {
@Param({"Sun, 27 Jan 2016 19:18:46 GMT", "Sun, 27 Dec 2016 19:18:46 GMT"})
String DATE_STRING;
@Benchmark
public Date parseHttpHeaderDateFormatterNew() {
return DateFormatter.parseHttpDate(DATE_STRING);
}
/*
@Benchmark
public Date parseHttpHeaderDateFormatter() {
return DateFormatterOld.parseHttpDate(DATE_STRING);
}
*/
/*
* Benchmark (DATE_STRING) Mode Cnt Score Error Units
* parseHttpHeaderDateFormatter Sun, 27 Jan 2016 19:18:46 GMT thrpt 6 4142781.221 ± 82155.002 ops/s
* parseHttpHeaderDateFormatter Sun, 27 Dec 2016 19:18:46 GMT thrpt 6 3781810.558 ± 38679.061 ops/s
* parseHttpHeaderDateFormatterNew Sun, 27 Jan 2016 19:18:46 GMT thrpt 6 4372569.705 ± 30257.537 ops/s
* parseHttpHeaderDateFormatterNew Sun, 27 Dec 2016 19:18:46 GMT thrpt 6 4339785.100 ± 57542.660 ops/s
*/
/*Old DateFormatter.tryParseMonth method:
private boolean tryParseMonth(CharSequence txt, int tokenStart, int tokenEnd) {
int len = tokenEnd - tokenStart;
if (len != 3) {
return false;
}
if (matchMonth("Jan", txt, tokenStart)) {
month = Calendar.JANUARY;
} else if (matchMonth("Feb", txt, tokenStart)) {
month = Calendar.FEBRUARY;
} else if (matchMonth("Mar", txt, tokenStart)) {
month = Calendar.MARCH;
} else if (matchMonth("Apr", txt, tokenStart)) {
month = Calendar.APRIL;
} else if (matchMonth("May", txt, tokenStart)) {
month = Calendar.MAY;
} else if (matchMonth("Jun", txt, tokenStart)) {
month = Calendar.JUNE;
} else if (matchMonth("Jul", txt, tokenStart)) {
month = Calendar.JULY;
} else if (matchMonth("Aug", txt, tokenStart)) {
month = Calendar.AUGUST;
} else if (matchMonth("Sep", txt, tokenStart)) {
month = Calendar.SEPTEMBER;
} else if (matchMonth("Oct", txt, tokenStart)) {
month = Calendar.OCTOBER;
} else if (matchMonth("Nov", txt, tokenStart)) {
month = Calendar.NOVEMBER;
} else if (matchMonth("Dec", txt, tokenStart)) {
month = Calendar.DECEMBER;
} else {
return false;
}
return true;
}
*/
}