All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.lucene.util.packed.XPackedInts Maven / Gradle / Ivy

There is a newer version: 2.18.0
Show newest version
/*
 * 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.util.packed;

import org.apache.lucene.codecs.CodecUtil;
import org.apache.lucene.store.DataInput;
import org.apache.lucene.store.DataOutput;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.util.RamUsageEstimator;
import org.apache.lucene.util.packed.PackedInts.Decoder;
import org.apache.lucene.util.packed.PackedInts.Encoder;
import org.apache.lucene.util.packed.PackedInts.Format;
import org.apache.lucene.util.packed.PackedInts.FormatAndBits;
import org.apache.lucene.util.packed.PackedInts.Reader;
import org.apache.lucene.util.packed.PackedInts.ReaderIterator;
import org.apache.lucene.util.packed.PackedInts.Writer;

import java.io.EOFException;
import java.io.IOException;
import java.util.Arrays;

/**
 * Forked from Lucene 8.x; removed in Lucene 8.9
 * 

* Todo: further investigate a better alternative *

* Simplistic compression for array of unsigned long values. Each value is {@code >= 0} and {@code * <=} a specified maximum value. The values are stored as packed ints, with each value consuming a * fixed number of bits. */ public class XPackedInts { /** At most 700% memory overhead, always select a direct implementation. */ public static final float FASTEST = 7f; /** At most 50% memory overhead, always select a reasonably fast implementation. */ public static final float FAST = 0.5f; /** At most 25% memory overhead. */ public static final float DEFAULT = 0.25f; /** No memory overhead at all, but the returned implementation may be slow. */ public static final float COMPACT = 0f; /** Default amount of memory to use for bulk operations. */ public static final int DEFAULT_BUFFER_SIZE = 1024; // 1K public static final String CODEC_NAME = "PackedInts"; public static final int VERSION_MONOTONIC_WITHOUT_ZIGZAG = 2; public static final int VERSION_START = VERSION_MONOTONIC_WITHOUT_ZIGZAG; public static final int VERSION_CURRENT = VERSION_MONOTONIC_WITHOUT_ZIGZAG; /** Check the validity of a version number. */ public static void checkVersion(int version) { if (version < VERSION_START) { throw new IllegalArgumentException("Version is too old, should be at least " + VERSION_START + " (got " + version + ")"); } else if (version > VERSION_CURRENT) { throw new IllegalArgumentException("Version is too new, should be at most " + VERSION_CURRENT + " (got " + version + ")"); } } /** * Try to find the {@link Format} and number of bits per value that would restore from disk the * fastest reader whose overhead is less than acceptableOverheadRatio. * *

The acceptableOverheadRatio parameter makes sense for random-access {@link * Reader}s. In case you only plan to perform sequential access on this stream later on, you * should probably use {@link PackedInts#COMPACT}. * *

If you don't know how many values you are going to write, use valueCount = -1. */ public static FormatAndBits fastestFormatAndBits(int valueCount, int bitsPerValue, float acceptableOverheadRatio) { if (valueCount == -1) { valueCount = Integer.MAX_VALUE; } acceptableOverheadRatio = Math.max(COMPACT, acceptableOverheadRatio); acceptableOverheadRatio = Math.min(FASTEST, acceptableOverheadRatio); float acceptableOverheadPerValue = acceptableOverheadRatio * bitsPerValue; // in bits int maxBitsPerValue = bitsPerValue + (int) acceptableOverheadPerValue; int actualBitsPerValue = -1; // rounded number of bits per value are usually the fastest if (bitsPerValue <= 8 && maxBitsPerValue >= 8) { actualBitsPerValue = 8; } else if (bitsPerValue <= 16 && maxBitsPerValue >= 16) { actualBitsPerValue = 16; } else if (bitsPerValue <= 32 && maxBitsPerValue >= 32) { actualBitsPerValue = 32; } else if (bitsPerValue <= 64 && maxBitsPerValue >= 64) { actualBitsPerValue = 64; } else { actualBitsPerValue = bitsPerValue; } return new FormatAndBits(Format.PACKED, actualBitsPerValue); } final static class XPackedWriter extends XWriter { boolean finished; final PackedInts.Format format; final BulkOperation encoder; final byte[] nextBlocks; final long[] nextValues; final int iterations; int off; int written; XPackedWriter(PackedInts.Format format, DataOutput out, int valueCount, int bitsPerValue, int mem) { super(out, valueCount, bitsPerValue); this.format = format; encoder = BulkOperation.of(format, bitsPerValue); iterations = encoder.computeIterations(valueCount, mem); nextBlocks = new byte[iterations * encoder.byteBlockCount()]; nextValues = new long[iterations * encoder.byteValueCount()]; off = 0; written = 0; finished = false; } @Override protected PackedInts.Format getFormat() { return format; } @Override public void add(long v) throws IOException { assert PackedInts.unsignedBitsRequired(v) <= bitsPerValue; assert !finished; if (valueCount != -1 && written >= valueCount) { throw new EOFException("Writing past end of stream"); } nextValues[off++] = v; if (off == nextValues.length) { flush(); } ++written; } @Override public void finish() throws IOException { assert !finished; if (valueCount != -1) { while (written < valueCount) { add(0L); } } flush(); finished = true; } private void flush() throws IOException { encoder.encode(nextValues, 0, nextBlocks, 0, iterations); final int blockCount = (int) format.byteCount(PackedInts.VERSION_CURRENT, off, bitsPerValue); out.writeBytes(nextBlocks, blockCount); Arrays.fill(nextValues, 0L); off = 0; } @Override public int ord() { return written - 1; } } /** * A packed integer array that can be modified. * */ public abstract static class Mutable extends Reader { /** * @return the number of bits used to store any given value. Note: This does not imply that * memory usage is {@code bitsPerValue * #values} as implementations are free to use * non-space-optimal packing of bits. */ public abstract int getBitsPerValue(); /** * Set the value at the given index in the array. * * @param index where the value should be positioned. * @param value a value conforming to the constraints set by the array. */ public abstract void set(int index, long value); /** * Bulk set: set at least one and at most len longs starting at off in * arr into this mutable, starting at index. Returns the actual number * of values that have been set. */ public int set(int index, long[] arr, int off, int len) { assert len > 0 : "len must be > 0 (got " + len + ")"; assert index >= 0 && index < size(); len = Math.min(len, size() - index); assert off + len <= arr.length; for (int i = index, o = off, end = index + len; i < end; ++i, ++o) { set(i, arr[o]); } return len; } /** * Fill the mutable from fromIndex (inclusive) to toIndex (exclusive) * with val. */ public void fill(int fromIndex, int toIndex, long val) { assert val <= maxValue(getBitsPerValue()); assert fromIndex <= toIndex; for (int i = fromIndex; i < toIndex; ++i) { set(i, val); } } /** Sets all values to 0. */ public void clear() { fill(0, size(), 0); } /** * Save this mutable into out. Instantiating a reader from the generated data will * return a reader with the same number of bits per value. */ public void save(DataOutput out) throws IOException { XWriter writer = getWriterNoHeader(out, getFormat(), size(), getBitsPerValue(), DEFAULT_BUFFER_SIZE); writer.writeHeader(); for (int i = 0; i < size(); ++i) { writer.add(get(i)); } writer.finish(); } /** The underlying format. */ Format getFormat() { return Format.PACKED; } } /** * A simple base for Readers that keeps track of valueCount and bitsPerValue. * */ abstract static class ReaderImpl extends Reader { protected final int valueCount; protected ReaderImpl(int valueCount) { this.valueCount = valueCount; } @Override public abstract long get(int index); @Override public final int size() { return valueCount; } } abstract static class MutableImpl extends Mutable { protected final int valueCount; protected final int bitsPerValue; protected MutableImpl(int valueCount, int bitsPerValue) { this.valueCount = valueCount; assert bitsPerValue > 0 && bitsPerValue <= 64 : "bitsPerValue=" + bitsPerValue; this.bitsPerValue = bitsPerValue; } @Override public final int getBitsPerValue() { return bitsPerValue; } @Override public final int size() { return valueCount; } @Override public String toString() { return getClass().getSimpleName() + "(valueCount=" + valueCount + ",bitsPerValue=" + bitsPerValue + ")"; } } /** A {@link Reader} which has all its values equal to 0 (bitsPerValue = 0). */ public static final class NullReader extends Reader { private final int valueCount; /** Sole constructor. */ public NullReader(int valueCount) { this.valueCount = valueCount; } @Override public long get(int index) { return 0; } @Override public int get(int index, long[] arr, int off, int len) { assert len > 0 : "len must be > 0 (got " + len + ")"; assert index >= 0 && index < valueCount; len = Math.min(len, valueCount - index); Arrays.fill(arr, off, off + len, 0); return len; } @Override public int size() { return valueCount; } @Override public long ramBytesUsed() { return RamUsageEstimator.alignObjectSize(RamUsageEstimator.NUM_BYTES_OBJECT_HEADER + Integer.BYTES); } } /** * A write-once Writer. * */ public abstract static class XWriter extends Writer { protected XWriter(DataOutput out, int valueCount, int bitsPerValue) { super(out, valueCount, bitsPerValue); } void writeHeader() throws IOException { assert valueCount != -1; CodecUtil.writeHeader(out, CODEC_NAME, VERSION_CURRENT); out.writeVInt(bitsPerValue); out.writeVInt(valueCount); out.writeVInt(getFormat().getId()); } } /** * Get a {@link Decoder}. * * @param format the format used to store packed ints * @param version the compatibility version * @param bitsPerValue the number of bits per value * @return a decoder */ public static Decoder getDecoder(Format format, int version, int bitsPerValue) { checkVersion(version); return BulkOperation.of(format, bitsPerValue); } /** * Get an {@link Encoder}. * * @param format the format used to store packed ints * @param version the compatibility version * @param bitsPerValue the number of bits per value * @return an encoder */ public static Encoder getEncoder(Format format, int version, int bitsPerValue) { checkVersion(version); return BulkOperation.of(format, bitsPerValue); } /** * Expert: Restore a {@link Reader} from a stream without reading metadata at the beginning of the * stream. This method is useful to restore data from streams which have been created using {@link * XPackedInts#getWriterNoHeader(DataOutput, Format, int, int, int)}. * * @param in the stream to read data from, positioned at the beginning of the packed values * @param format the format used to serialize * @param version the version used to serialize the data * @param valueCount how many values the stream holds * @param bitsPerValue the number of bits per value * @return a Reader * @throws IOException If there is a low-level I/O error * @see XPackedInts#getWriterNoHeader(DataOutput, Format, int, int, int) */ public static Reader getReaderNoHeader(DataInput in, Format format, int version, int valueCount, int bitsPerValue) throws IOException { checkVersion(version); switch (format) { case PACKED_SINGLE_BLOCK: return XPacked64SingleBlock.create(in, valueCount, bitsPerValue); case PACKED: return new XPacked64(version, in, valueCount, bitsPerValue); default: throw new AssertionError("Unknown Writer format: " + format); } } /** * Restore a {@link Reader} from a stream. * * @param in the stream to read data from * @return a Reader * @throws IOException If there is a low-level I/O error */ public static Reader getReader(DataInput in) throws IOException { final int version = CodecUtil.checkHeader(in, CODEC_NAME, VERSION_START, VERSION_CURRENT); final int bitsPerValue = in.readVInt(); assert bitsPerValue > 0 && bitsPerValue <= 64 : "bitsPerValue=" + bitsPerValue; final int valueCount = in.readVInt(); final Format format = Format.byId(in.readVInt()); return getReaderNoHeader(in, format, version, valueCount, bitsPerValue); } /** * Expert: Restore a {@link ReaderIterator} from a stream without reading metadata at the * beginning of the stream. This method is useful to restore data from streams which have been * created using {@link XPackedInts#getWriterNoHeader(DataOutput, Format, int, int, int)}. * * @param in the stream to read data from, positioned at the beginning of the packed values * @param format the format used to serialize * @param version the version used to serialize the data * @param valueCount how many values the stream holds * @param bitsPerValue the number of bits per value * @param mem how much memory the iterator is allowed to use to read-ahead (likely to speed up * iteration) * @return a ReaderIterator * @see XPackedInts#getWriterNoHeader(DataOutput, Format, int, int, int) */ public static ReaderIterator getReaderIteratorNoHeader( DataInput in, Format format, int version, int valueCount, int bitsPerValue, int mem ) { checkVersion(version); return new PackedReaderIterator(format, version, valueCount, bitsPerValue, in, mem); } /** * Retrieve PackedInts as a {@link ReaderIterator} * * @param in positioned at the beginning of a stored packed int structure. * @param mem how much memory the iterator is allowed to use to read-ahead (likely to speed up * iteration) * @return an iterator to access the values * @throws IOException if the structure could not be retrieved. */ public static ReaderIterator getReaderIterator(DataInput in, int mem) throws IOException { final int version = CodecUtil.checkHeader(in, CODEC_NAME, VERSION_START, VERSION_CURRENT); final int bitsPerValue = in.readVInt(); assert bitsPerValue > 0 && bitsPerValue <= 64 : "bitsPerValue=" + bitsPerValue; final int valueCount = in.readVInt(); final Format format = Format.byId(in.readVInt()); return getReaderIteratorNoHeader(in, format, version, valueCount, bitsPerValue, mem); } /** * Expert: Construct a direct {@link Reader} from a stream without reading metadata at the * beginning of the stream. This method is useful to restore data from streams which have been * created using {@link XPackedInts#getWriterNoHeader(DataOutput, Format, int, int, int)}. * *

The returned reader will have very little memory overhead, but every call to {@link * Reader#get(int)} is likely to perform a disk seek. * * @param in the stream to read data from * @param format the format used to serialize * @param version the version used to serialize the data * @param valueCount how many values the stream holds * @param bitsPerValue the number of bits per value * @return a direct Reader */ public static Reader getDirectReaderNoHeader(final IndexInput in, Format format, int version, int valueCount, int bitsPerValue) { checkVersion(version); switch (format) { case PACKED: return new DirectPackedReader(bitsPerValue, valueCount, in); case PACKED_SINGLE_BLOCK: return new DirectPacked64SingleBlockReader(bitsPerValue, valueCount, in); default: throw new AssertionError("Unknown format: " + format); } } /** * Construct a direct {@link Reader} from an {@link IndexInput}. This method is useful to restore * data from streams which have been created using {@link XPackedInts#getWriter(DataOutput, int, * int, float)}. * *

The returned reader will have very little memory overhead, but every call to {@link * Reader#get(int)} is likely to perform a disk seek. * * @param in the stream to read data from * @return a direct Reader * @throws IOException If there is a low-level I/O error */ public static Reader getDirectReader(IndexInput in) throws IOException { final int version = CodecUtil.checkHeader(in, CODEC_NAME, VERSION_START, VERSION_CURRENT); final int bitsPerValue = in.readVInt(); assert bitsPerValue > 0 && bitsPerValue <= 64 : "bitsPerValue=" + bitsPerValue; final int valueCount = in.readVInt(); final Format format = Format.byId(in.readVInt()); return getDirectReaderNoHeader(in, format, version, valueCount, bitsPerValue); } /** * Create a packed integer array with the given amount of values initialized to 0. the valueCount * and the bitsPerValue cannot be changed after creation. All Mutables known by this factory are * kept fully in RAM. * *

Positive values of acceptableOverheadRatio will trade space for speed by * selecting a faster but potentially less memory-efficient implementation. An * acceptableOverheadRatio of {@link PackedInts#COMPACT} will make sure that the most * memory-efficient implementation is selected whereas {@link PackedInts#FASTEST} will make sure * that the fastest implementation is selected. * * @param valueCount the number of elements * @param bitsPerValue the number of bits available for any given value * @param acceptableOverheadRatio an acceptable overhead ratio per value * @return a mutable packed integer array */ public static Mutable getMutable(int valueCount, int bitsPerValue, float acceptableOverheadRatio) { final FormatAndBits formatAndBits = fastestFormatAndBits(valueCount, bitsPerValue, acceptableOverheadRatio); return getMutable(valueCount, formatAndBits.bitsPerValue, formatAndBits.format); } /** * Same as {@link #getMutable(int, int, float)} with a pre-computed number of bits per value and * format. * */ public static Mutable getMutable(int valueCount, int bitsPerValue, PackedInts.Format format) { assert valueCount >= 0; switch (format) { case PACKED_SINGLE_BLOCK: return XPacked64SingleBlock.create(valueCount, bitsPerValue); case PACKED: return new XPacked64(valueCount, bitsPerValue); default: throw new AssertionError(); } } /** * Expert: Create a packed integer array writer for the given output, format, value count, and * number of bits per value. * *

The resulting stream will be long-aligned. This means that depending on the format which is * used, up to 63 bits will be wasted. An easy way to make sure that no space is lost is to always * use a valueCount that is a multiple of 64. * *

This method does not write any metadata to the stream, meaning that it is your * responsibility to store it somewhere else in order to be able to recover data from the stream * later on: * *

    *
  • format (using {@link Format#getId()}), *
  • valueCount, *
  • bitsPerValue, *
  • {@link #VERSION_CURRENT}. *
* *

It is possible to start writing values without knowing how many of them you are actually * going to write. To do this, just pass -1 as valueCount. On the other * hand, for any positive value of valueCount, the returned writer will make sure * that you don't write more values than expected and pad the end of stream with zeros in case you * have written less than valueCount when calling {@link Writer#finish()}. * *

The mem parameter lets you control how much memory can be used to buffer * changes in memory before flushing to disk. High values of mem are likely to * improve throughput. On the other hand, if speed is not that important to you, a value of * 0 will use as little memory as possible and should already offer reasonable throughput. * * @param out the data output * @param format the format to use to serialize the values * @param valueCount the number of values * @param bitsPerValue the number of bits per value * @param mem how much memory (in bytes) can be used to speed up serialization * @return a Writer * @see XPackedInts#getReaderIteratorNoHeader(DataInput, Format, int, int, int, int) * @see XPackedInts#getReaderNoHeader(DataInput, Format, int, int, int) */ public static XWriter getWriterNoHeader(DataOutput out, Format format, int valueCount, int bitsPerValue, int mem) { return new XPackedWriter(format, out, valueCount, bitsPerValue, mem); } /** * Create a packed integer array writer for the given output, format, value count, and number of * bits per value. * *

The resulting stream will be long-aligned. This means that depending on the format which is * used under the hoods, up to 63 bits will be wasted. An easy way to make sure that no space is * lost is to always use a valueCount that is a multiple of 64. * *

This method writes metadata to the stream, so that the resulting stream is sufficient to * restore a {@link Reader} from it. You don't need to track valueCount or * bitsPerValue by yourself. In case this is a problem, you should probably look at {@link * #getWriterNoHeader(DataOutput, Format, int, int, int)}. * *

The acceptableOverheadRatio parameter controls how readers that will be * restored from this stream trade space for speed by selecting a faster but potentially less * memory-efficient implementation. An acceptableOverheadRatio of {@link * PackedInts#COMPACT} will make sure that the most memory-efficient implementation is selected * whereas {@link PackedInts#FASTEST} will make sure that the fastest implementation is selected. * In case you are only interested in reading this stream sequentially later on, you should * probably use {@link PackedInts#COMPACT}. * * @param out the data output * @param valueCount the number of values * @param bitsPerValue the number of bits per value * @param acceptableOverheadRatio an acceptable overhead ratio per value * @return a Writer * @throws IOException If there is a low-level I/O error */ public static Writer getWriter(DataOutput out, int valueCount, int bitsPerValue, float acceptableOverheadRatio) throws IOException { assert valueCount >= 0; final FormatAndBits formatAndBits = fastestFormatAndBits(valueCount, bitsPerValue, acceptableOverheadRatio); final XWriter writer = getWriterNoHeader(out, formatAndBits.format, valueCount, formatAndBits.bitsPerValue, DEFAULT_BUFFER_SIZE); writer.writeHeader(); return writer; } /** * Returns how many bits are required to hold values up to and including maxValue NOTE: This * method returns at least 1. * * @param maxValue the maximum value that should be representable. * @return the amount of bits needed to represent values from 0 to maxValue. */ public static int bitsRequired(long maxValue) { if (maxValue < 0) { throw new IllegalArgumentException("maxValue must be non-negative (got: " + maxValue + ")"); } return unsignedBitsRequired(maxValue); } /** * Returns how many bits are required to store bits, interpreted as an unsigned * value. NOTE: This method returns at least 1. * */ public static int unsignedBitsRequired(long bits) { return Math.max(1, 64 - Long.numberOfLeadingZeros(bits)); } /** * Calculates the maximum unsigned long that can be expressed with the given number of bits. * * @param bitsPerValue the number of bits available for any given value. * @return the maximum value for the given bits. */ public static long maxValue(int bitsPerValue) { return bitsPerValue == 64 ? Long.MAX_VALUE : ~(~0L << bitsPerValue); } /** * Copy src[srcPos:srcPos+len] into dest[destPos:destPos+len] using at * most mem bytes. */ public static void copy(Reader src, int srcPos, Mutable dest, int destPos, int len, int mem) { assert srcPos + len <= src.size(); assert destPos + len <= dest.size(); final int capacity = mem >>> 3; if (capacity == 0) { for (int i = 0; i < len; ++i) { dest.set(destPos++, src.get(srcPos++)); } } else if (len > 0) { // use bulk operations final long[] buf = new long[Math.min(capacity, len)]; copy(src, srcPos, dest, destPos, len, buf); } } /** * Same as {@link #copy(Reader, int, Mutable, int, int, int)} but using a pre-allocated buffer. */ static void copy(Reader src, int srcPos, Mutable dest, int destPos, int len, long[] buf) { assert buf.length > 0; int remaining = 0; while (len > 0) { final int read = src.get(srcPos, buf, remaining, Math.min(len, buf.length - remaining)); assert read > 0; srcPos += read; len -= read; remaining += read; final int written = dest.set(destPos, buf, 0, remaining); assert written > 0; destPos += written; if (written < remaining) { System.arraycopy(buf, written, buf, 0, remaining - written); } remaining -= written; } while (remaining > 0) { final int written = dest.set(destPos, buf, 0, remaining); destPos += written; remaining -= written; System.arraycopy(buf, written, buf, 0, remaining); } } /** * Check that the block size is a power of 2, in the right bounds, and return its log in base 2. */ static int checkBlockSize(int blockSize, int minBlockSize, int maxBlockSize) { if (blockSize < minBlockSize || blockSize > maxBlockSize) { throw new IllegalArgumentException("blockSize must be >= " + minBlockSize + " and <= " + maxBlockSize + ", got " + blockSize); } if ((blockSize & (blockSize - 1)) != 0) { throw new IllegalArgumentException("blockSize must be a power of two, got " + blockSize); } return Integer.numberOfTrailingZeros(blockSize); } /** * Return the number of blocks required to store size values on blockSize * . */ static int numBlocks(long size, int blockSize) { final int numBlocks = (int) (size / blockSize) + (size % blockSize == 0 ? 0 : 1); if ((long) numBlocks * blockSize < size) { throw new IllegalArgumentException("size is too large for this block size"); } return numBlocks; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy