From 975e209244aa0ac9a19f613a1e7ff1d4382d1b95 Mon Sep 17 00:00:00 2001 From: Daniel Gredler Date: Mon, 11 May 2026 23:24:19 +0000 Subject: [PATCH] 8380794: AttributedString performance Reviewed-by: jlu, naoto --- .../classes/java/text/AttributedString.java | 144 +++++------------- .../java/text/AttributedStringBench.java | 125 +++++++++++++++ 2 files changed, 163 insertions(+), 106 deletions(-) create mode 100644 test/micro/org/openjdk/bench/java/text/AttributedStringBench.java diff --git a/src/java.base/share/classes/java/text/AttributedString.java b/src/java.base/share/classes/java/text/AttributedString.java index 0333efde81b..52e5b5dba3c 100644 --- a/src/java.base/share/classes/java/text/AttributedString.java +++ b/src/java.base/share/classes/java/text/AttributedString.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -46,7 +46,6 @@ import java.text.AttributedCharacterIterator.Attribute; * @see Annotation * @since 1.2 */ - public class AttributedString { // field holding the text String text; @@ -54,12 +53,11 @@ public class AttributedString { // Fields holding run attribute information. // Run attributes are organized by run. // Arrays are always of equal lengths (the current capacity). - // Since there are no vectors of int, we have to use arrays. + // Since there are not yet Lists of unboxed int, we use arrays. private static final int INITIAL_CAPACITY = 10; - int runCount; // actual number of runs, <= current capacity - int[] runStarts; // start index for each run - Vector[] runAttributes; // vector of attribute keys for each run - Vector[] runAttributeValues; // parallel vector of attribute values for each run + int runCount; // actual number of runs, <= current capacity + int[] runStarts; // start index for each run + Map[] runAttributes; // attributes for each run /** * Constructs an AttributedString instance with the given @@ -152,16 +150,8 @@ public class AttributedString { int attributeCount = attributes.size(); if (attributeCount > 0) { - createRunAttributeDataVectors(); - Vector newRunAttributes = new Vector<>(attributeCount); - Vector newRunAttributeValues = new Vector<>(attributeCount); - runAttributes[0] = newRunAttributes; - runAttributeValues[0] = newRunAttributeValues; - - for (Map.Entry entry : attributes.entrySet()) { - newRunAttributes.addElement(entry.getKey()); - newRunAttributeValues.addElement(entry.getValue()); - } + createRunAttributeDataArrays(); + runAttributes[0] = new HashMap<>(attributes); } } @@ -250,12 +240,13 @@ public class AttributedString { // Select attribute keys to be taken care of HashSet keys = new HashSet<>(); + Set textKeys = text.getAllAttributeKeys(); if (attributes == null) { - keys.addAll(text.getAllAttributeKeys()); + keys.addAll(textKeys); } else { for (int i = 0; i < attributes.length; i++) keys.add(attributes[i]); - keys.retainAll(text.getAllAttributeKeys()); + keys.retainAll(textKeys); } if (keys.isEmpty()) return; @@ -376,9 +367,9 @@ public class AttributedString { throw new IllegalArgumentException("Can't add attribute to 0-length text"); } - // make sure we have run attribute data vectors + // make sure we have run attribute data arrays if (runCount == 0) { - createRunAttributeDataVectors(); + createRunAttributeDataArrays(); } // break up runs if necessary @@ -393,9 +384,9 @@ public class AttributedString { private synchronized void addAttributeImpl(Attribute attribute, Object value, int beginIndex, int endIndex) { - // make sure we have run attribute data vectors + // make sure we have run attribute data arrays if (runCount == 0) { - createRunAttributeDataVectors(); + createRunAttributeDataArrays(); } // break up runs if necessary @@ -405,19 +396,14 @@ public class AttributedString { addAttributeRunData(attribute, value, beginRunIndex, endRunIndex); } - private final void createRunAttributeDataVectors() { + private final void createRunAttributeDataArrays() { // use temporary variables so things remain consistent in case of an exception int[] newRunStarts = new int[INITIAL_CAPACITY]; - @SuppressWarnings("unchecked") - Vector[] newRunAttributes = (Vector[]) new Vector[INITIAL_CAPACITY]; - - @SuppressWarnings("unchecked") - Vector[] newRunAttributeValues = (Vector[]) new Vector[INITIAL_CAPACITY]; + Map[] newRunAttributes = (Map[]) new Map[INITIAL_CAPACITY]; runStarts = newRunStarts; runAttributes = newRunAttributes; - runAttributeValues = newRunAttributeValues; runCount = 1; // assume initial run starting at index 0 } @@ -463,29 +449,21 @@ public class AttributedString { // use temporary variables so things remain consistent in case of an exception int[] newRunStarts = Arrays.copyOf(runStarts, newCapacity); - Vector[] newRunAttributes = + Map[] newRunAttributes = Arrays.copyOf(runAttributes, newCapacity); - Vector[] newRunAttributeValues = - Arrays.copyOf(runAttributeValues, newCapacity); runStarts = newRunStarts; runAttributes = newRunAttributes; - runAttributeValues = newRunAttributeValues; } // make copies of the attribute information of the old run that the new one used to be part of // use temporary variables so things remain consistent in case of an exception - Vector newRunAttributes = null; - Vector newRunAttributeValues = null; + Map newRunAttributes = null; if (copyAttrs) { - Vector oldRunAttributes = runAttributes[runIndex - 1]; - Vector oldRunAttributeValues = runAttributeValues[runIndex - 1]; + Map oldRunAttributes = runAttributes[runIndex - 1]; if (oldRunAttributes != null) { - newRunAttributes = new Vector<>(oldRunAttributes); - } - if (oldRunAttributeValues != null) { - newRunAttributeValues = new Vector<>(oldRunAttributeValues); + newRunAttributes = new HashMap<>(oldRunAttributes); } } @@ -494,11 +472,9 @@ public class AttributedString { for (int i = runCount - 1; i > runIndex; i--) { runStarts[i] = runStarts[i - 1]; runAttributes[i] = runAttributes[i - 1]; - runAttributeValues[i] = runAttributeValues[i - 1]; } runStarts[runIndex] = offset; runAttributes[runIndex] = newRunAttributes; - runAttributeValues[runIndex] = newRunAttributeValues; return runIndex; } @@ -508,32 +484,13 @@ public class AttributedString { int beginRunIndex, int endRunIndex) { for (int i = beginRunIndex; i < endRunIndex; i++) { - int keyValueIndex = -1; // index of key and value in our vectors; assume we don't have an entry yet - if (runAttributes[i] == null) { - Vector newRunAttributes = new Vector<>(); - Vector newRunAttributeValues = new Vector<>(); + Map attributes = runAttributes[i]; + if (attributes == null) { + Map newRunAttributes = new HashMap<>(); runAttributes[i] = newRunAttributes; - runAttributeValues[i] = newRunAttributeValues; - } else { - // check whether we have an entry already - keyValueIndex = runAttributes[i].indexOf(attribute); - } - - if (keyValueIndex == -1) { - // create new entry - int oldSize = runAttributes[i].size(); - runAttributes[i].addElement(attribute); - try { - runAttributeValues[i].addElement(value); - } - catch (Exception e) { - runAttributes[i].setSize(oldSize); - runAttributeValues[i].setSize(oldSize); - } - } else { - // update existing entry - runAttributeValues[i].set(keyValueIndex, value); + attributes = newRunAttributes; } + attributes.put(attribute, value); } } @@ -596,18 +553,11 @@ public class AttributedString { } private synchronized Object getAttribute(Attribute attribute, int runIndex) { - Vector currentRunAttributes = runAttributes[runIndex]; - Vector currentRunAttributeValues = runAttributeValues[runIndex]; + Map currentRunAttributes = runAttributes[runIndex]; if (currentRunAttributes == null) { return null; } - int attributeIndex = currentRunAttributes.indexOf(attribute); - if (attributeIndex != -1) { - return currentRunAttributeValues.elementAt(attributeIndex); - } - else { - return null; - } + return currentRunAttributes.get(attribute); } // gets an attribute value, but returns an annotation only if it's range does not extend outside the range beginIndex..endIndex @@ -680,22 +630,11 @@ public class AttributedString { */ private void setAttributes(Map attrs, int offset) { if (runCount == 0) { - createRunAttributeDataVectors(); + createRunAttributeDataArrays(); } - int index = ensureRunBreak(offset, false); - int size; - - if (attrs != null && (size = attrs.size()) > 0) { - Vector runAttrs = new Vector<>(size); - Vector runValues = new Vector<>(size); - - for (Map.Entry entry : attrs.entrySet()) { - runAttrs.add(entry.getKey()); - runValues.add(entry.getValue()); - } - runAttributes[index] = runAttrs; - runAttributeValues[index] = runValues; + if (attrs != null && !attrs.isEmpty()) { + runAttributes[index] = new HashMap<>(attrs); } } @@ -945,18 +884,13 @@ public class AttributedString { // ??? should try to create this only once, then update if necessary, // and give callers read-only view Set keys = new HashSet<>(); - int i = 0; - while (i < runCount) { + for (int i = 0; i < runCount; i++) { if (runStarts[i] < endIndex && (i == runCount - 1 || runStarts[i + 1] > beginIndex)) { - Vector currentRunAttributes = runAttributes[i]; + Map currentRunAttributes = runAttributes[i]; if (currentRunAttributes != null) { - int j = currentRunAttributes.size(); - while (j-- > 0) { - keys.add(currentRunAttributes.get(j)); - } + keys.addAll(currentRunAttributes.keySet()); } } - i++; } return keys; } @@ -1040,10 +974,10 @@ public class AttributedString { public Set> entrySet() { HashSet> set = new HashSet<>(); synchronized (AttributedString.this) { - int size = runAttributes[runIndex].size(); - for (int i = 0; i < size; i++) { - Attribute key = runAttributes[runIndex].get(i); - Object value = runAttributeValues[runIndex].get(i); + Map attributes = runAttributes[runIndex]; + for (Map.Entry entry : attributes.entrySet()) { + Attribute key = entry.getKey(); + Object value = entry.getValue(); if (value instanceof Annotation) { value = AttributedString.this.getAttributeCheckRange(key, runIndex, beginIndex, endIndex); @@ -1051,9 +985,7 @@ public class AttributedString { continue; } } - - Map.Entry entry = new AttributeEntry(key, value); - set.add(entry); + set.add(new AttributeEntry(key, value)); } } return set; diff --git a/test/micro/org/openjdk/bench/java/text/AttributedStringBench.java b/test/micro/org/openjdk/bench/java/text/AttributedStringBench.java new file mode 100644 index 00000000000..bc96760882c --- /dev/null +++ b/test/micro/org/openjdk/bench/java/text/AttributedStringBench.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.text; + +import java.awt.Color; +import java.awt.font.TextAttribute; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +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.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(3) +@State(Scope.Thread) +public class AttributedStringBench { + + private AttributedString string1; + private AttributedString string2; + private AttributedString string3; + + @Setup + public void setup() { + + string1 = new AttributedString("this is an attributed string test"); + + string2 = new AttributedString("this is an attributed string test"); + string2.addAttribute(TextAttribute.SIZE, 20); + + string3 = new AttributedString("this is an attributed string test"); + string3.addAttribute(TextAttribute.SIZE, 20); + string3.addAttribute(TextAttribute.FOREGROUND, Color.BLACK); + string3.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_REGULAR); + string3.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, 11, 21); + } + + @Benchmark + public void iterateMissingWithNoAttributes(Blackhole blackhole) { + iterate(string1, TextAttribute.BIDI_EMBEDDING, blackhole); + } + + @Benchmark + public void iterateMissingWithOneAttribute(Blackhole blackhole) { + iterate(string2, TextAttribute.BIDI_EMBEDDING, blackhole); + } + + @Benchmark + public void iterateMissingWithThreeAttributes(Blackhole blackhole) { + iterate(string3, TextAttribute.BIDI_EMBEDDING, blackhole); + } + + @Benchmark + public void iteratePresentWithOneAttribute(Blackhole blackhole) { + iterate(string2, TextAttribute.SIZE, blackhole); + } + + @Benchmark + public void iteratePresentWithThreeAttributes(Blackhole blackhole) { + iterate(string3, TextAttribute.SIZE, blackhole); + } + + private static void iterate(AttributedString as, TextAttribute att, Blackhole blackhole) { + AttributedCharacterIterator it = as.getIterator(); + for (char c = it.first(); c != AttributedCharacterIterator.DONE; c = it.next()) { + Object val = it.getAttribute(att); + blackhole.consume(val); + } + } + + @Benchmark + public void createWithNoAttributes(Blackhole blackhole) { + AttributedString string = new AttributedString("this is an attributed string test"); + blackhole.consume(string); + } + + @Benchmark + public void createWithOneAttribute(Blackhole blackhole) { + AttributedString string = new AttributedString("this is an attributed string test"); + string.addAttribute(TextAttribute.SIZE, 20); + blackhole.consume(string); + } + + @Benchmark + public void createWithThreeAttributes(Blackhole blackhole) { + AttributedString string = new AttributedString("this is an attributed string test"); + string.addAttribute(TextAttribute.SIZE, 20); + string.addAttribute(TextAttribute.FOREGROUND, Color.BLACK); + string.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_REGULAR); + string.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, 11, 21); + blackhole.consume(string); + } +}