From c295c33a267bd316c24e3573cf3a17f78dd4a5a0 Mon Sep 17 00:00:00 2001 From: Dmitry Konstantinov Date: Mon, 15 Dec 2025 13:13:55 +0000 Subject: [PATCH] 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 --- CHANGES.txt | 1 + .../cassandra/serializers/UTF8Serializer.java | 52 +++++- .../test/microbench/UTF8ValidatorBench.java | 117 +++++++++++++ .../serializers/UTF8ValidatorTest.java | 165 ++++++++++++++++++ 4 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 test/microbench/org/apache/cassandra/test/microbench/UTF8ValidatorBench.java create mode 100644 test/unit/org/apache/cassandra/serializers/UTF8ValidatorTest.java diff --git a/CHANGES.txt b/CHANGES.txt index 2be609ddf6..3a2759ee5f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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) diff --git a/src/java/org/apache/cassandra/serializers/UTF8Serializer.java b/src/java/org/apache/cassandra/serializers/UTF8Serializer.java index 7b071e7131..3caafdc639 100644 --- a/src/java/org/apache/cassandra/serializers/UTF8Serializer.java +++ b/src/java/org/apache/cassandra/serializers/UTF8Serializer.java @@ -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 boolean validateByteArray(byte[] valueAsArray, int start, int end, + V value, ValueAccessor 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 boolean validateSlowPath(V value, ValueAccessor accessor, int offset) + { + int b; State state = State.START; while (!accessor.isEmptyFromOffset(value, offset)) { diff --git a/test/microbench/org/apache/cassandra/test/microbench/UTF8ValidatorBench.java b/test/microbench/org/apache/cassandra/test/microbench/UTF8ValidatorBench.java new file mode 100644 index 0000000000..06db9d9df0 --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/UTF8ValidatorBench.java @@ -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); + } +} diff --git a/test/unit/org/apache/cassandra/serializers/UTF8ValidatorTest.java b/test/unit/org/apache/cassandra/serializers/UTF8ValidatorTest.java new file mode 100644 index 0000000000..f10815f79e --- /dev/null +++ b/test/unit/org/apache/cassandra/serializers/UTF8ValidatorTest.java @@ -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; + } +}