Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lucene/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ Optimizations
* GITHUB#16370: Sort two-phase iterators by matchCost in DenseConjunctionBulkScorer.
(Alan Woodward)

* GITHUB#16381: StopFilter: pack short ASCII stop words into longs for LongHashSet lookup,
avoiding char[] pointer chasing. 2.6x faster lookups on English stop words. (Costin Leau)

Bug Fixes
---------------------
* GITHUB#16350: Disable bulk-scoring in monitor queries. (Alan Woodward)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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.lucene.benchmark.jmh;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import org.apache.lucene.analysis.CharArraySet;
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.Warmup;
import org.openjdk.jmh.infra.Blackhole;

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class StopFilterBenchmark {

private static final List<String> ENGLISH_STOP_WORDS =
Arrays.asList(
"a", "an", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is",
"it", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there",
"these", "they", "this", "to", "was", "will", "with");

private static final String[] CONTENT_WORDS = {
"lucene", "search", "index", "query", "document", "field", "segment", "merge", "filter",
"score", "boost", "term", "token", "analyzer", "reader", "writer", "directory", "codec",
"postings", "collection", "virtual"
};

@Param({"english", "technical"})
String textType;

private CharArraySet baseline;
private CharArraySet packed;
private char[][] lookupTokens;
private int[] lookupLengths;

@Setup(Level.Trial)
public void setup() {
baseline = new CharArraySet(ENGLISH_STOP_WORDS, true, 0);
packed = new CharArraySet(ENGLISH_STOP_WORDS, true);

Random rng = new Random(42);
int tokenCount = 2000;
lookupTokens = new char[tokenCount][];
lookupLengths = new int[tokenCount];
double stopWordRatio = textType.equals("english") ? 0.50 : 0.20;

for (int i = 0; i < tokenCount; i++) {
if (rng.nextDouble() < stopWordRatio) {
String sw = ENGLISH_STOP_WORDS.get(rng.nextInt(ENGLISH_STOP_WORDS.size()));
lookupTokens[i] = sw.toCharArray();
} else {
String cw = CONTENT_WORDS[rng.nextInt(CONTENT_WORDS.length)];
lookupTokens[i] = cw.toCharArray();
}
lookupLengths[i] = lookupTokens[i].length;
}
}

@Benchmark
public void baseline(Blackhole bh) {
for (int i = 0; i < lookupTokens.length; i++) {
bh.consume(baseline.contains(lookupTokens[i], 0, lookupLengths[i]));
}
}

@Benchmark
public void packed(Blackhole bh) {
for (int i = 0; i < lookupTokens.length; i++) {
bh.consume(packed.contains(lookupTokens[i], 0, lookupLengths[i]));
}
}
}
82 changes: 82 additions & 0 deletions lucene/core/src/java/org/apache/lucene/analysis/CharArrayMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.internal.hppc.LongHashSet;
import org.apache.lucene.util.CharsRef;

/**
Expand All @@ -36,8 +37,13 @@ public class CharArrayMap<V> extends AbstractMap<Object, V> {
private static final CharArrayMap<?> EMPTY_MAP = new EmptyCharArrayMap<>();

private static final int INIT_SIZE = 8;
static final int DEFAULT_MAX_CACHED_LENGTH = 8;

private final boolean ignoreCase;
private final int maxCachedLength;
private int count;
private LongHashSet packedKeys;

// package private because used in CharArraySet's non Set-conform CharArraySetIterator
char[][] keys;
// package private because used in CharArraySet's non Set-conform CharArraySetIterator
Expand All @@ -52,7 +58,23 @@ public class CharArrayMap<V> extends AbstractMap<Object, V> {
*/
@SuppressWarnings("unchecked")
public CharArrayMap(int startSize, boolean ignoreCase) {
this(startSize, ignoreCase, DEFAULT_MAX_CACHED_LENGTH);
}

/**
* Create map with enough capacity to hold startSize terms. When {@code ignoreCase} is true, keys
* up to {@code maxCachedLength} ASCII characters are cached in a compact representation for
* faster lookup, avoiding per-character case conversion overhead. Set to 0 to disable.
*
* @param startSize the initial capacity
* @param ignoreCase <code>false</code> if and only if the set should be case sensitive otherwise
* <code>true</code>.
* @param maxCachedLength maximum ASCII length to cache for faster lookup (0 to disable)
*/
@SuppressWarnings("unchecked")
public CharArrayMap(int startSize, boolean ignoreCase, int maxCachedLength) {
this.ignoreCase = ignoreCase;
this.maxCachedLength = maxCachedLength;
int size = INIT_SIZE;
while (startSize + (startSize >> 2) > size) {
size <<= 1;
Expand All @@ -78,7 +100,9 @@ private CharArrayMap(CharArrayMap<V> toCopy) {
this.keys = toCopy.keys;
this.values = toCopy.values;
this.ignoreCase = toCopy.ignoreCase;
this.maxCachedLength = toCopy.maxCachedLength;
this.count = toCopy.count;
this.packedKeys = toCopy.packedKeys;
}

/**
Expand All @@ -88,6 +112,7 @@ private CharArrayMap(CharArrayMap<V> toCopy) {
@Override
public void clear() {
count = 0;
packedKeys = null;
Arrays.fill(keys, null);
Arrays.fill(values, null);
}
Expand All @@ -97,11 +122,19 @@ public void clear() {
* {@link #keySet()}
*/
public boolean containsKey(char[] text, int off, int len) {
if (packedKeys != null) {
long packed = pack(text, off, len, ignoreCase);
return packed >= 0 && packedKeys.contains(packed);
}
return keys[getSlot(text, off, len)] != null;
}

/** true if the <code>CharSequence</code> is in the {@link #keySet()} */
public boolean containsKey(CharSequence cs) {
if (packedKeys != null) {
long packed = packCharSequence(cs, ignoreCase);
return packed >= 0 && packedKeys.contains(packed);
}
return keys[getSlot(cs)] != null;
}

Expand All @@ -118,11 +151,19 @@ public boolean containsKey(Object o) {
* <code>off</code>
*/
public V get(char[] text, int off, int len) {
if (packedKeys != null) {
long packed = pack(text, off, len, ignoreCase);
if (packed < 0 || !packedKeys.contains(packed)) return null;
}
return values[getSlot(text, off, len)];
}

/** returns the value of the mapping of the chars inside this {@code CharSequence} */
public V get(CharSequence cs) {
if (packedKeys != null) {
long packed = packCharSequence(cs, ignoreCase);
if (packed < 0 || !packedKeys.contains(packed)) return null;
}
return values[getSlot(cs)];
}

Expand Down Expand Up @@ -191,6 +232,7 @@ public V put(char[] text, V value) {
if (ignoreCase) {
CharacterUtils.toLowerCase(text, 0, text.length);
}
packedKeys = null;
int slot = getSlot(text, 0, text.length);
if (keys[slot] != null) {
final V oldValue = values[slot];
Expand Down Expand Up @@ -297,6 +339,46 @@ private int getHashCode(CharSequence text) {
return code;
}

void tryBuildPackedKeys() {
if (!ignoreCase || maxCachedLength == 0) return;
LongHashSet set = new LongHashSet(count);
for (char[] key : keys) {
if (key == null) continue;
long packed = pack(key, 0, key.length, false);
if (packed < 0) {
packedKeys = null;
return;
}
set.add(packed);
}
packedKeys = set;
}

private long pack(char[] buf, int off, int len, boolean toLowerCase) {
if (len > maxCachedLength || len == 0) return -1;
long packed = 0;
for (int i = 0; i < len; i++) {
char c = buf[off + i];
if (c > 127) return -1;
if (toLowerCase && c >= 'A' && c <= 'Z') c = (char) (c | 0x20);
packed = (packed << 8) | c;
}
return packed;
}

private long packCharSequence(CharSequence cs, boolean toLowerCase) {
int len = cs.length();
if (len > maxCachedLength || len == 0) return -1;
long packed = 0;
for (int i = 0; i < len; i++) {
char c = cs.charAt(i);
if (c > 127) return -1;
if (toLowerCase && c >= 'A' && c <= 'Z') c = (char) (c | 0x20);
packed = (packed << 8) | c;
}
return packed;
}

@Override
public V remove(Object key) {
throw new UnsupportedOperationException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,21 @@ public CharArraySet(int startSize, boolean ignoreCase) {
* <code>true</code>.
*/
public CharArraySet(Collection<?> c, boolean ignoreCase) {
this(c.size(), ignoreCase);
this(c, ignoreCase, CharArrayMap.DEFAULT_MAX_CACHED_LENGTH);
}

/**
* Creates a set from a Collection of objects.
*
* @param c a collection whose elements to be placed into the set
* @param ignoreCase <code>false</code> if and only if the set should be case sensitive otherwise
* <code>true</code>.
* @param maxCachedLength maximum ASCII length to cache for faster lookup (0 to disable)
*/
public CharArraySet(Collection<?> c, boolean ignoreCase, int maxCachedLength) {
this(new CharArrayMap<>(c.size(), ignoreCase, maxCachedLength));
addAll(c);
map.tryBuildPackedKeys();
}

/**
Expand Down
Loading