Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UNDERTOW-2210] Improved write ASCII path on ServletPrintWriter #1424

Merged
merged 9 commits into from
Jan 24, 2023
200 changes: 200 additions & 0 deletions benchmarks/src/main/java/io/undertow/benchmarks/AsciiEncoders.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package io.undertow.benchmarks;
franz1981 marked this conversation as resolved.
Show resolved Hide resolved

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class AsciiEncoders {

public interface BufferFlusher {
void flushBuffer(ByteBuffer buffer) throws IOException;
}

public interface AsciiEncoder {

int writeAndFlushAscii(BufferFlusher flusher, ByteBuffer buffer, char[] chars, int start, int end) throws IOException;

}

public enum BatchFixedBufferOffsetAsciiEncoder implements AsciiEncoder {
Instance;

@Override
public int writeAndFlushAscii(BufferFlusher flusher, ByteBuffer buffer, char[] chars, int start, int end) throws IOException {
final ByteOrder order = buffer.order();
int i = start;
while (i < end) {
final int bufferPos = buffer.position();
final int bufferRemaining = buffer.remaining();
final int sRemaining = end - i;
final int remaining = Math.min(sRemaining, bufferRemaining);
final int written = order == ByteOrder.LITTLE_ENDIAN ?
setAsciiLE(buffer, bufferPos, chars, i, remaining) :
setAsciiBE(buffer, bufferPos, chars, i, remaining);
i += written;
buffer.position(bufferPos + written);
if (!buffer.hasRemaining()) {
flusher.flushBuffer(buffer);
}
// we have given up with the fast path? slow path NOW!
if (written < remaining) {
return i;
}
}
return i;
}

private static int setAsciiLE(ByteBuffer buffer, int out, char[] chars, int off, int len) {
final int longRounds = len >>> 3;
for (int i = 0; i < longRounds; i++) {
final char c0 = chars[off];
final char c1 = chars[off + 1];
final char c2 = chars[off + 2];
final char c3 = chars[off + 3];
final char c4 = chars[off + 4];
final char c5 = chars[off + 5];
final char c6 = chars[off + 6];
final char c7 = chars[off + 7];
if (c7 > 127 || c6 > 127 || c5 > 127 || c4 > 127 ||
c3 > 127 || c2 > 127 || c1 > 127 || c0 > 127) {
return i << 3;
}
final long leBatch = (((long) (c7) << 56) |
((long) (c6 & 0xff) << 48) |
((long) (c5 & 0xff) << 40) |
((long) (c4 & 0xff) << 32) |
((long) (c3 & 0xff) << 24) |
((long) (c2 & 0xff) << 16) |
((long) (c1 & 0xff) << 8) |
((long) (c0 & 0xff)));
buffer.putLong(out, leBatch);
out += Long.BYTES;
off += Long.BYTES;
}
final int byteRounds = len & 7;
if (byteRounds > 0) {
for (int i = 0; i < byteRounds; i++) {
final char c = chars[off + i];
if (c > 127) {
return (longRounds << 3) + i;
}
buffer.put(out + i, (byte) c);
}
}
return len;
}

private static int setAsciiBE(ByteBuffer buffer, int out, char[] chars, int off, int len) {
final int longRounds = len >>> 3;
for (int i = 0; i < longRounds; i++) {
final char c0 = chars[off];
final char c1 = chars[off + 1];
final char c2 = chars[off + 2];
final char c3 = chars[off + 3];
final char c4 = chars[off + 4];
final char c5 = chars[off + 5];
final char c6 = chars[off + 6];
final char c7 = chars[off + 7];
if (c7 > 127 || c6 > 127 || c5 > 127 || c4 > 127 ||
c3 > 127 || c2 > 127 || c1 > 127 || c0 > 127) {
return i << 3;
}
final long leBatch = (((long) (c0) << 56) |
((long) (c1 & 0xff) << 48) |
((long) (c2 & 0xff) << 40) |
((long) (c3 & 0xff) << 32) |
((long) (c4 & 0xff) << 24) |
((long) (c5 & 0xff) << 16) |
((long) (c6 & 0xff) << 8) |
((long) (c7 & 0xff)));
buffer.putLong(out, leBatch);
out += Long.BYTES;
off += Long.BYTES;
}
final int byteRounds = len & 7;
if (byteRounds > 0) {
for (int i = 0; i < byteRounds; i++) {
final char c = chars[off + i];
if (c > 127) {
return (longRounds << 3) + i;
}
buffer.put(out + i, (byte) c);
}
}
return len;
}
}

public enum NonBatchFixedBufferOffsetAsciiEncoder implements AsciiEncoder {
Instance;

@Override
public int writeAndFlushAscii(BufferFlusher flusher, ByteBuffer buffer, char[] chars, int start, int end) throws IOException {
int i = start;
while (i < end) {
final int bufferPos = buffer.position();
final int bufferRemaining = buffer.remaining();
final int sRemaining = end - i;
final int remaining = Math.min(sRemaining, bufferRemaining);
final int written = setAscii(buffer, bufferPos, chars, i, remaining);
i += written;
buffer.position(bufferPos + written);
if (!buffer.hasRemaining()) {
flusher.flushBuffer(buffer);
}
// we have given up with the fast path? slow path NOW!
if (written < remaining) {
return i;
}
}
return i;
}

private static int setAscii(ByteBuffer buffer, int out, char[] chars, int off, int len) {
for (int i = 0; i < len; i++) {
final char c = chars[off + i];
if (c > 127) {
return i;
}
buffer.put(out + i, (byte) c);
}
return len;
}
}

public enum NonBatchMutableBufferOffsetAsciiEncoder implements AsciiEncoder {
Instance;

@Override
public int writeAndFlushAscii(BufferFlusher flusher, ByteBuffer buffer, char[] chars, int start, int end) throws IOException {
//fast path, basically we are hoping this is ascii only
int remaining = buffer.remaining();
boolean ok = true;
//so we have a pure ascii buffer, just write it out and skip all the encoder cost
int i = start;
int flushPos = i + remaining;
while (ok && i < end) {
int realEnd = Math.min(end, flushPos);
for (; i < realEnd; ++i) {
char c = chars[i];
if (c > 127) {
ok = false;
break;
} else {
buffer.put((byte) c);
}
}
if (i == flushPos) {
flusher.flushBuffer(buffer);
flushPos = i + buffer.remaining();
}
}
if (ok) {
return end - start;
}
return -1;
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2019 Red Hat, Inc., and individual contributors
franz1981 marked this conversation as resolved.
Show resolved Hide resolved
* as indicated by the @author tags.
*
* Licensed 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.undertow.benchmarks;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.CompilerControl;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.SplittableRandom;
import java.util.concurrent.TimeUnit;

@State(Scope.Benchmark)
@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS)
@Warmup(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS)
@Fork(2)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class AsciiEncodingBenchmark implements AsciiEncoders.BufferFlusher {

private static final int ASCII_GEN_SEED = 0;
@Param({"7", "19", "248", "12392", "493727"})
private int inLength;
@Param({"8196"})
private int outCapacity;

@Param({"batch", "noBatch", "vanilla"})
private String encoderType;
private ByteBuffer out;
private char[] input;

private AsciiEncoders.AsciiEncoder encoder;

@Setup
public void init() {
out = ByteBuffer.allocateDirect(outCapacity).order(ByteOrder.BIG_ENDIAN);
SplittableRandom random = new SplittableRandom(ASCII_GEN_SEED);
input = new char[inLength];
for (int i = 0; i < inLength; i++) {
input[i] = (char) random.nextInt(0, 128);
}
switch (encoderType) {
case "batch":
encoder = AsciiEncoders.BatchFixedBufferOffsetAsciiEncoder.Instance;
break;
case "noBatch":
encoder = AsciiEncoders.NonBatchFixedBufferOffsetAsciiEncoder.Instance;
break;
case "vanilla":
encoder = AsciiEncoders.NonBatchMutableBufferOffsetAsciiEncoder.Instance;
break;
}
}

@Override
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void flushBuffer(ByteBuffer buffer) throws IOException {
buffer.position(0);
}

@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int encode() throws IOException {
final ByteBuffer out = this.out;
final char[] input = this.input;
out.clear();
return encoder.writeAndFlushAscii(this, out, this.input, 0, input.length);
}
}
Loading