blob: 2e900db85c774b469e64015c68adba072c725a71 [file] [view]
<!--
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.
-->
# ALP Float/Double Codec Design
Status: draft for the C++-first implementation on `colin/alp-codec`.
## Scope
- This work is C++-only. The Java implementation is intentionally untouched.
- ALP is an opt-in encoding for `FLOAT` and `DOUBLE` only.
- The existing Gorilla default is preserved. ALP is enabled through an
experimental build option until the format is accepted by the community.
- The format is language-neutral so a Java decoder can be added later without
changing encoded bytes.
- Python will reuse the C++ implementation through the C wrapper.
## Relationship to TS_2DIFF
ALP must not reuse the TS_2DIFF float-adaptation path. In particular it must not
use TS_2DIFF's page precision (`maxPointNumber`), page overflow bitmap,
`page_blocks_` buffering, or `PageMeta` parsing.
ALP has its own block metadata because the algorithm needs it: exponent/factor
indices, frame-of-reference base, bit width, and exceptions. This metadata is
block-local and is part of the ALP payload. A TsFile page is only a container;
the ALP block boundary is a flush boundary.
## Wire Format (version 1)
All integers are little-endian. `value_size` is 4 for `FLOAT` and 8 for
`DOUBLE`.
### Page header
| field | size | description |
|---|---:|---|
| version | 1 | format version, currently 1 |
| data_type | 1 | 0 = float, 1 = double |
| reserved | 2 | zero |
| value_count | 4 | total values in the page |
| block_count | 4 | number of ALP blocks |
| reserved | 4 | zero |
### Block header
| field | size | description |
|---|---:|---|
| scheme | 1 | 0 = ALP, 1 = PLAIN fallback |
| factor | 1 | ALP factor index |
| exponent | 1 | ALP exponent index |
| flags | 1 | bit 0 = exceptions present |
| value_count | 4 | values in this block |
| exception_count | 4 | number of exceptions |
| body_bytes | 4 | padded body size in bytes |
| for_base | 8 | signed frame-of-reference base, int32 for float and int64 for double |
| bit_width | 1 | 0..32 for float, 0..64 for double |
| reserved | 3 | zero |
The header is followed by:
- for `scheme == ALP` with exceptions: an exception bitmap of
`ceil(value_count / 8)` bytes, followed by `exception_count` raw
`value_size` values in increasing position order;
- for `scheme == ALP`: the bit-packed body;
- for `scheme == PLAIN`: `value_count * value_size` raw values.
The packed body is padded with zero bytes to a multiple of 32 bytes after
adding 16 bytes of guard space, so SIMD/word loads may safely read past the
logical end of the stream. `body_bytes` records the padded size.
### Bit packing
The body stores adjusted integers using an LSB-first word stream. For float
each word is 32 bits; for double each word is 64 bits. Value `i` occupies
`bit_width` bits starting at bit `i * bit_width`. A zero bit width means every
non-exception value equals `for_base`.
This layout is deliberately different from TsFile's existing MSB-first pack8
layout. It is defined by the scalar reference implementation, and SIMD kernels
must produce and consume the same bytes.
## Encoding
1. Buffer values per page.
2. Split the buffered values into blocks of at most 1024 values.
3. For each block, sample values and choose the best `(factor, exponent)`
combination using the estimated compressed size.
4. Encode each value with the chosen pair. A value is an exception when it is
non-finite, `-0.0`, outside the safe integer range, or when the reference
decode does not reproduce the original bit pattern.
5. Compute the frame-of-reference base and bit width over non-exception
integers.
6. Estimate whether ALP is smaller than PLAIN. If not, emit the block as
`PLAIN`.
7. Emit the block header, exception data, and packed body.
The encoder's exception check must use the same decode routine as the decoder.
This is required for correctness: a value classified as a non-exception must
be bit-exactly reproducible by the decoder.
## Decoding
1. Parse and validate the page header.
2. For each block, parse and validate the block header.
3. For `PLAIN`, copy raw values.
4. For `ALP`, unpack adjusted integers, add `for_base`, apply the reference
decode, and overwrite exception positions from the exception array.
5. Validate that the decoded value count matches the page header.
6. Implement `read_batch_float` and `read_batch_double` so the SIMD path can
work on whole blocks rather than one value at a time.
## SIMD Plan
SIMDe is already a TsFile C++ dependency. The ALP kernels will use the existing
`ENABLE_SIMD` switch and `TsFile::SIMDe` target.
- Write the kernels with `simde__m256i` and `simde_mm256_*`, matching the
existing encoding code style.
- Compile an AVX2 kernel translation unit with `-mavx2` or `/arch:AVX2` on
x86. Do not apply AVX2 flags to baseline TsFile translation units.
- On AArch64 the same SIMDe source maps to NEON without the AVX2 flag.
- Add a scalar fallback used when SIMD is disabled or unavailable.
- Dispatch once per block through a function table. Do not branch per value.
- Add an optional AVX-512 translation unit later. The wire format must not
depend on the availability of AVX-512.
## Testing
- Scalar round-trip tests for float and double.
- Boundary values: NaN, +Inf, -Inf, -0.0, subnormals, smallest/largest
magnitudes, exact zero, and values forcing exceptions.
- Constant blocks, single-value blocks, partial final blocks, and multi-block
pages.
- Scalar and SIMD must encode to identical bytes and decode to identical
values.
- Corrupt/truncated payload tests must return errors without out-of-bounds
access.
- Benchmarks compare ALP against Gorilla for decimal, normalized, smooth,
repeated, and high-entropy data.