Optimize UTF8Validator.validate for ASCII prefixed Strings

Use a plain loop to check if it is ASCII symbol before going into more complicated UTF8 parsing.
Avoid ValueAccessor to get extra boost for the ASCII check, especially in non-monomorphic cases.

Patch by Dmitry Konstantinov; reviewed by Jyothsna Konisa, Stefan Miklosovic for CASSANDRA-21075
This commit is contained in:
Dmitry Konstantinov 2025-12-15 13:13:55 +00:00
parent d7bd75303f
commit c295c33a26
4 changed files with 333 additions and 2 deletions

View File

@ -1,4 +1,5 @@
5.1
* Optimize UTF8Validator.validate for ASCII prefixed Strings (CASSANDRA-21075)
* Switch LatencyMetrics to use ThreadLocalTimer/ThreadLocalCounter (CASSANDRA-21080)
* Accord: write rejections would be returned to users as server errors rather than INVALID and TxnReferenceOperation didn't handle all collections prperly (CASSANDRA-21061)
* Use byte[] directly in QueryOptions instead of ByteBuffer and convert them to ArrayCell instead of BufferCell to reduce allocations (CASSANDRA-20166)

View File

@ -17,8 +17,11 @@
*/
package org.apache.cassandra.serializers;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import org.apache.cassandra.db.marshal.ByteArrayAccessor;
import org.apache.cassandra.db.marshal.ByteBufferAccessor;
import org.apache.cassandra.db.marshal.ValueAccessor;
public class UTF8Serializer extends AbstractTextSerializer
@ -57,8 +60,53 @@ public class UTF8Serializer extends AbstractTextSerializer
if (value == null)
return false;
int b = 0;
int offset = 0;
// perf optimizations:
// 1) avoid bimorphic/megamorphic calls via ValueAccessor
// 2) use a simplified logic to handle ASCII prefixed String scenario faster
if (accessor == ByteArrayAccessor.instance)
{
byte[] valueAsArray = accessor.toArray(value);
return validateByteArray(valueAsArray, 0, valueAsArray.length, value, accessor);
}
if (accessor == ByteBufferAccessor.instance)
{
ByteBuffer valueAsBuffer = accessor.toBuffer(value);
if (valueAsBuffer.hasArray())
{
byte[] valueAsArray = valueAsBuffer.array();
int start = valueAsBuffer.position();
int end = start + valueAsBuffer.remaining();
return validateByteArray(valueAsArray, start, end, value, accessor);
}
}
int end = accessor.size(value);
for (int i = 0; i < end; i++)
{
if (accessor.getByte(value, i) < 0)
{
return validateSlowPath(value, accessor, i);
}
}
return true;
}
private static <V> boolean validateByteArray(byte[] valueAsArray, int start, int end,
V value, ValueAccessor<V> accessor)
{
assert start >= 0 && end <= valueAsArray.length;
for (int i = start; i < end; i++)
{
if (valueAsArray[i] < 0)
return validateSlowPath(value, accessor, i - start);
}
return true;
}
private static <V> boolean validateSlowPath(V value, ValueAccessor<V> accessor, int offset)
{
int b;
State state = State.START;
while (!accessor.isEmptyFromOffset(value, offset))
{

View File

@ -0,0 +1,117 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 org.apache.cassandra.test.microbench;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import org.apache.cassandra.db.marshal.ByteArrayAccessor;
import org.apache.cassandra.db.marshal.ByteBufferAccessor;
import org.apache.cassandra.serializers.UTF8Serializer;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
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.Threads;
import org.openjdk.jmh.annotations.Warmup;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3, jvmArgsAppend = "-Xmx512M")
@Threads(1)
@State(Scope.Benchmark)
public class UTF8ValidatorBench
{
@Param({ "short ASCII", "long ASCII", "short ASCII prefix non-ASCII", "short non-ASCII", "long non-ASCII"})
private String stringType;
byte[] arrayValue;
ByteBuffer heapByteBufferValue;
@Setup(Level.Trial)
public void setup() throws Throwable
{
switch (stringType)
{
case "short ASCII":
arrayValue = "ASCII string".getBytes(StandardCharsets.UTF_8);
break;
case "long ASCII":
arrayValue = ("ASCII is an acronym for American Standard Code for Information Interchange, " +
"is a character encoding standard for representing a particular set of 95 " +
"(English language focused) printable and 33 control characters a total of 128 code points. " +
"The set of available punctuation had significant impact on the syntax of computer languages " +
"and text markup. ASCII hugely influenced the design of character sets used by modern computers; " +
"for example, the first 128 code points of Unicode are the same as ASCII.").getBytes(StandardCharsets.UTF_8);
break;
case "short ASCII prefix non-ASCII":
arrayValue = "a hierarchy of number systems: ⊆ ℕ₀ ⊂ ".getBytes(StandardCharsets.UTF_8);
break;
case "short non-ASCII":
arrayValue = " ⊆ ℕ₀ ⊂ ".getBytes(StandardCharsets.UTF_8);
break;
case "long non-ASCII": // https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html
arrayValue = ("⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞\n" +
" ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎\n" +
" ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂\n" +
" ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙\n" +
" ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑ \n" +
" ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲").getBytes(StandardCharsets.UTF_8);
break;
default:
throw new UnsupportedOperationException();
}
heapByteBufferValue = ByteBuffer.allocate(arrayValue.length);
heapByteBufferValue.put(arrayValue).rewind();
}
@Benchmark
public void testBimorphic()
{
UTF8Serializer.instance.validate(heapByteBufferValue, ByteBufferAccessor.instance);
UTF8Serializer.instance.validate(arrayValue, ByteArrayAccessor.instance);
}
@Benchmark
public void testMonomorphicArray()
{
UTF8Serializer.instance.validate(arrayValue, ByteArrayAccessor.instance);
UTF8Serializer.instance.validate(arrayValue, ByteArrayAccessor.instance);
}
@Benchmark
public void testMonomorphicHeapByteBuffer()
{
UTF8Serializer.instance.validate(heapByteBufferValue, ByteBufferAccessor.instance);
UTF8Serializer.instance.validate(heapByteBufferValue, ByteBufferAccessor.instance);
}
}

View File

@ -0,0 +1,165 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 org.apache.cassandra.serializers;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import org.junit.Test;
import org.apache.cassandra.db.marshal.ByteArrayAccessor;
import org.apache.cassandra.db.marshal.ByteBufferAccessor;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
// https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html
public class UTF8ValidatorTest
{
@Test
public void testValidStrings()
{
assertValidUtf8String("");
assertValidUtf8String("ASCII text");
assertValidUtf8String("\n\r");
assertValidUtf8String("a hierarchy of number systems: ⊆ ℕ₀ ⊂ ");
assertValidUtf8String(" ⊆ ℕ₀ ⊂ ");
assertValidUtf8String("⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞\n" +
" ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎\n" +
" ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂\n" +
" ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙\n" +
" ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑ \n" +
" ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲");
assertValidUtf8String("Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese\n" +
" classic 'San Gua'):\n" +
"\n" +
" [----------------------------|------------------------]\n" +
" ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช พระปกเกศกองบู๊กู้ขึ้นใหม่\n" +
" สิบสองกษัตริย์ก่อนหน้าแลถัดไป สององค์ไซร้โง่เขลาเบาปัญญา\n" +
" ทรงนับถือขันทีเป็นที่พึ่ง บ้านเมืองจึงวิปริตเป็นนักหนา\n" +
" โฮจิ๋นเรียกทัพทั่วหัวเมืองมา หมายจะฆ่ามดชั่วตัวสำคัญ\n" +
" เหมือนขับไสไล่เสือจากเคหา รับหมาป่าเข้ามาเลยอาสัญ\n" +
" ฝ่ายอ้องอุ้นยุแยกให้แตกกัน ใช้สาวนั้นเป็นชนวนชื่นชวนใจ\n" +
" พลันลิฉุยกุยกีกลับก่อเหตุ ช่างอาเพศจริงหนาฟ้าร้องไห้\n" +
" ต้องรบราฆ่าฟันจนบรรลัย ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ");
assertValidUtf8String("ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ");
assertValidUtf8String("Box drawing alignment tests: █\n" +
"\n" +
" ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n" +
" ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n" +
" ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n" +
" ╠╡ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n" +
" ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n" +
" ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n" +
" ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n");
}
@Test // https://www.w3.org/2001/06/utf-8-wrong/UTF-8-test.html
public void testInvalidStrings()
{
// continuation bytes only
assertInvalidUtf8String(0x80);
assertInvalidUtf8String(0xbf);
assertInvalidUtf8String(0x80,0x80);
// Bad trailing bytes
assertInvalidUtf8String(0xF0, 0xA4, 0xAD, 0x7F);
assertInvalidUtf8String(0xF0, 0xA4, 0xAD, 0x7F);
// first bytes of 2-byte sequences (0xc0-0xdf), each followed by a space character
assertInvalidUtf8String(0xc0, ' ', 0xdf, ' ');
// first bytes of 3-byte sequences (0xe0-0xef), each followed by a space character
assertInvalidUtf8String(0xe0, ' ', 0xe1, ' ');
// first bytes of 4-byte sequences (0xf0-0xf7), each followed by a space character
assertInvalidUtf8String(0xf0, ' ', 0xf7, ' ');
// first bytes of 5-byte sequences (0xf8-0xfb), each followed by a space character
assertInvalidUtf8String(0xf8, ' ', 0xfb, ' ');
// first bytes of 6-byte sequences (0xfc-0xfd), each followed by a space character
assertInvalidUtf8String(0xfc, ' ', 0xfd, ' ');
// Impossible bytes
assertInvalidUtf8String(0xfe);
assertInvalidUtf8String(0xff);
assertInvalidUtf8String(0xfe, 0xfe, 0xff, 0xff);
// Sequences with last continuation byte missing
assertInvalidUtf8String(0xc0);
assertInvalidUtf8String(0xe0, 0x80);
// Maximum overlong sequences
assertInvalidUtf8String(0xc1, 0xbf);
// 'ASCII' + continuation byte at the end
assertInvalidUtf8String(0x41, 0x53, 0x43, 0x49, 0x49, 0x80);
}
public void assertValidUtf8String(String value)
{
byte[] byteArrayValue = value.getBytes(StandardCharsets.UTF_8);
ByteBuffer bufferValue = ByteBuffer.wrap(byteArrayValue);
ByteBuffer bufferValueInTheMiddle = ByteBuffer.allocate(byteArrayValue.length + 2 + 2);
wrapValueWithImpossibleBytes(bufferValueInTheMiddle, byteArrayValue);
ByteBuffer directBufferValue = ByteBuffer.allocateDirect(byteArrayValue.length);
directBufferValue.put(byteArrayValue);
directBufferValue.rewind();
ByteBuffer directBufferValueInTheMiddle = ByteBuffer.allocate(byteArrayValue.length + 2 + 2);
wrapValueWithImpossibleBytes(directBufferValueInTheMiddle, byteArrayValue);
assertTrue(UTF8Serializer.UTF8Validator.validate(byteArrayValue, ByteArrayAccessor.instance));
assertTrue(UTF8Serializer.UTF8Validator.validate(bufferValue, ByteBufferAccessor.instance));
assertTrue(UTF8Serializer.UTF8Validator.validate(bufferValueInTheMiddle, ByteBufferAccessor.instance));
assertTrue(UTF8Serializer.UTF8Validator.validate(directBufferValue, ByteBufferAccessor.instance));
assertTrue(UTF8Serializer.UTF8Validator.validate(bufferValueInTheMiddle, ByteBufferAccessor.instance));
}
private static void wrapValueWithImpossibleBytes(ByteBuffer bufferValueInTheMiddle, byte[] byteArrayValue)
{
// bufferValue wrapped by impossible bytes
// to ensure that validate method does not read outside of buffer boundaries
bufferValueInTheMiddle.put((byte)0xfe);
bufferValueInTheMiddle.put((byte)0xfe);
bufferValueInTheMiddle.put(byteArrayValue);
bufferValueInTheMiddle.put((byte)0xfe);
bufferValueInTheMiddle.put((byte)0xfe);
bufferValueInTheMiddle.rewind();
bufferValueInTheMiddle.position(2);
bufferValueInTheMiddle.limit(bufferValueInTheMiddle.limit() - 2);
}
public void assertInvalidUtf8String(int ... bytes)
{
byte[] byteArrayValue = toByteArray(bytes);
ByteBuffer bufferValue = ByteBuffer.wrap(byteArrayValue);
ByteBuffer directBufferValue = ByteBuffer.allocateDirect(byteArrayValue.length);
directBufferValue.put(byteArrayValue);
directBufferValue.rewind();
assertFalse(UTF8Serializer.UTF8Validator.validate(byteArrayValue, ByteArrayAccessor.instance));
assertFalse(UTF8Serializer.UTF8Validator.validate(bufferValue, ByteBufferAccessor.instance));
assertFalse(UTF8Serializer.UTF8Validator.validate(directBufferValue, ByteBufferAccessor.instance));
}
private static byte[] toByteArray(int... bytes)
{
byte[] value = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++)
value[i] = (byte) bytes[i];
return value;
}
}