blob: 612f75334e8bc99cde4c82e81b7c7f2b7d4458bb [file] [log] [blame]
// 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.
#include "kudu/common/columnar_serialization.h"
#ifdef __aarch64__
#include "kudu/util/sse2neon.h" // IWYU pragma: keep
#else
#include <emmintrin.h>
#include <immintrin.h>
#endif
#include <cstring>
#include <ostream>
#include <string> // IWYU pragma: keep
#include <type_traits>
#include <vector>
#include <glog/logging.h>
#include "kudu/common/columnblock.h"
#include "kudu/common/common.pb.h"
#include "kudu/common/rowblock.h"
#include "kudu/common/schema.h"
#include "kudu/common/types.h"
#include "kudu/common/zp7.h"
#include "kudu/gutil/cpu.h"
#include "kudu/gutil/port.h"
#include "kudu/gutil/strings/fastmem.h"
#include "kudu/util/alignment.h"
#include "kudu/util/bitmap.h"
#include "kudu/util/slice.h"
using std::vector;
namespace kudu {
namespace {
// Utility to write variable bit-length values to a pre-allocated buffer.
//
// This is similar to the BitWriter class in util/bit-stream-utils.h except that
// the other implementation manages growing an underlying 'faststring' rather
// than writing to existing memory.
struct BitWriter {
// Start writing data to 'dst', but skip over the first 'skip_initial_bits'
// bits.
BitWriter(uint8_t* dst, int skip_initial_bits) : dst_(dst) {
DCHECK_GE(skip_initial_bits, 0);
dst_ += skip_initial_bits / 8;
// The "skip" may place us in the middle of a byte. To simplify this,
// we just position ourselves at the start of that byte and buffer the
// pre-existing bits, thus positioning ourselves at the right offset.
int preexisting_bits = skip_initial_bits % 8;
uint8_t preexisting_val = *dst_ & ((1 << preexisting_bits) - 1);
Put(preexisting_val, preexisting_bits);
}
~BitWriter() {
CHECK(flushed_) << "must flush";
}
void Put(uint64_t v, int num_bits) {
DCHECK(!flushed_);
DCHECK_LE(num_bits, 64);
buffered_values_ |= v << num_buffered_bits_;
num_buffered_bits_ += num_bits;
if (PREDICT_FALSE(num_buffered_bits_ >= 64)) {
memcpy(dst_, &buffered_values_, 8);
buffered_values_ = 0;
num_buffered_bits_ -= 64;
int shift = num_bits - num_buffered_bits_;
buffered_values_ = (shift >= 64) ? 0 : v >> shift;
dst_ += 8;
}
DCHECK_LT(num_buffered_bits_, 64);
}
void Flush() {
CHECK(!flushed_) << "must only flush once";
while (num_buffered_bits_ > 0) {
*dst_++ = buffered_values_ & 0xff;
buffered_values_ >>= 8;
num_buffered_bits_ -= 8;
}
flushed_ = true;
}
uint8_t* dst_;
// Accumulated bits that haven't been flushed to the destination buffer yet.
uint64_t buffered_values_ = 0;
// The number of accumulated bits in buffered_values_.
int num_buffered_bits_ = 0;
bool flushed_ = false;
};
} // anonymous namespace
////////////////////////////////////////////////////////////
// ZeroNullValues
////////////////////////////////////////////////////////////
namespace internal {
namespace {
// Implementation of ZeroNullValues, specialized for a particular type size.
template<int sizeof_type>
ATTRIBUTE_NOINLINE
void ZeroNullValuesImpl(int dst_idx,
int n_rows,
uint8_t* __restrict__ dst_values_buf,
uint8_t* __restrict__ non_null_bitmap) {
int aligned_dst_idx = KUDU_ALIGN_DOWN(dst_idx, 8);
int aligned_n_sel = n_rows + (dst_idx - aligned_dst_idx);
uint8_t* aligned_values_base = dst_values_buf + aligned_dst_idx * sizeof_type;
// TODO(todd): this code path benefits from the BMI instruction set. We should
// compile it twice, once with BMI supported.
ForEachUnsetBit(non_null_bitmap + aligned_dst_idx/8,
aligned_n_sel,
[&](int position) {
// The position here is relative to our aligned bitmap.
memset(aligned_values_base + position * sizeof_type, 0, sizeof_type);
});
}
} // anonymous namespace
// Zero out any values in 'dst_values_buf' which are indicated as null in 'non_null_bitmap'.
//
// 'n_rows' cells are processed, starting at index 'dst_idx' within the buffers.
// 'sizeof_type' indicates the size of each cell in bytes.
//
// NOTE: this assumes that dst_values_buf and non_null_bitmap are valid for the full range
// of indices [0, dst_idx + n_rows). The implementation may redundantly re-zero cells
// at indexes less than dst_idx.
void ZeroNullValues(int sizeof_type,
int dst_idx,
int n_rows,
uint8_t* dst_values_buf,
uint8_t* dst_non_null_bitmap) {
// Delegate to specialized implementations for each type size.
// This changes variable-length memsets into inlinable single instructions.
switch (sizeof_type) {
#define CASE(size) \
case size: \
ZeroNullValuesImpl<size>(dst_idx, n_rows, dst_values_buf, dst_non_null_bitmap); \
break;
CASE(1);
CASE(2);
CASE(4);
CASE(8);
CASE(16);
#undef CASE
default:
LOG(FATAL) << "bad size: " << sizeof_type;
}
}
////////////////////////////////////////////////////////////
// CopyNonNullBitmap
////////////////////////////////////////////////////////////
namespace {
template<class PextImpl>
void CopyNonNullBitmapImpl(
const uint8_t* __restrict__ non_null_bitmap,
const uint8_t* __restrict__ sel_bitmap,
int dst_idx,
int n_rows,
uint8_t* __restrict__ dst_non_null_bitmap) {
BitWriter bw(dst_non_null_bitmap, dst_idx);
int num_64bit_words = n_rows / 64;
for (int i = 0; i < num_64bit_words; i++) {
uint64_t sel_mask = UnalignedLoad<uint64_t>(sel_bitmap + i * 8);
int num_bits = __builtin_popcountll(sel_mask);
uint64_t non_nulls = UnalignedLoad<uint64_t>(non_null_bitmap + i * 8);
uint64_t extracted = PextImpl::call(non_nulls, sel_mask);
bw.Put(extracted, num_bits);
}
int rem_rows = n_rows % 64;
non_null_bitmap += num_64bit_words * 8;
sel_bitmap += num_64bit_words * 8;
while (rem_rows > 0) {
uint8_t non_nulls = *non_null_bitmap;
uint8_t sel_mask = *sel_bitmap;
uint64_t extracted = PextImpl::call(non_nulls, sel_mask);
int num_bits = __builtin_popcountl(sel_mask);
bw.Put(extracted, num_bits);
sel_bitmap++;
non_null_bitmap++;
rem_rows -= 8;
}
bw.Flush();
}
struct PextZp7Clmul {
inline static uint64_t call(uint64_t val, uint64_t mask) {
return zp7_pext_64_clmul(val, mask);
}
};
struct PextZp7Simple {
inline static uint64_t call(uint64_t val, uint64_t mask) {
return zp7_pext_64_simple(val, mask);
}
};
#ifdef __x86_64__
struct PextInstruction {
__attribute__((target("bmi2")))
inline static uint64_t call(uint64_t val, uint64_t mask) {
#if !defined(__clang__) && defined(__GNUC__) && __GNUC__ < 5
// GCC <5 doesn't properly handle the _pext_u64 intrinsic inside
// a function with a specified target attribute. So, use inline
// assembly instead.
//
// Though this assembly works on clang as well, it has two downsides:
// - the "multiple constraint" 'rm' for 'mask' is supposed to indicate to
// the compiler that the mask could either be in memory or in a register,
// but clang doesn't support this, and will always spill it to memory
// even if the value is already in a register. That results in an extra couple
// cycles.
// - using the intrinsic means that clang optimization passes have some opportunity
// to better understand what's going on and make appropriate downstream optimizations.
uint64_t dst;
asm ("pextq %[mask], %[val], %[dst]"
: [dst] "=r" (dst)
: [val] "r" (val),
[mask] "rm" (mask));
return dst;
#else
return _pext_u64(val, mask);
#endif // compiler check
}
};
// Explicit instantiation of the template for the PextInstruction case
// allows us to apply the 'bmi2' target attribute for just this version.
template
__attribute__((target("bmi2")))
void CopyNonNullBitmapImpl<PextInstruction>(
const uint8_t* __restrict__ non_null_bitmap,
const uint8_t* __restrict__ sel_bitmap,
int dst_idx,
int n_rows,
uint8_t* __restrict__ dst_non_null_bitmap);
#endif // __x86_64__
} // anonymous namespace
// Return a prioritized list of methods that can be used for extracting bits from the non-null
// bitmap.
vector<PextMethod> GetAvailablePextMethods() {
vector<PextMethod> ret;
#ifdef __x86_64__
base::CPU cpu;
// Even though recent AMD chips support pext, it's extremely slow,
// so only use BMI2 on Intel, and instead use the 'zp7' software
// implementation on AMD.
if (cpu.has_bmi2() && cpu.vendor_name() == "GenuineIntel") {
ret.push_back(PextMethod::kPextInstruction);
}
if (cpu.has_pclmulqdq()) {
ret.push_back(PextMethod::kClmul);
}
#endif
ret.push_back(PextMethod::kSimple);
return ret;
}
PextMethod g_pext_method = GetAvailablePextMethods()[0];
void CopyNonNullBitmap(const uint8_t* non_null_bitmap,
const uint8_t* sel_bitmap,
int dst_idx,
int n_rows,
uint8_t* dst_non_null_bitmap) {
switch (g_pext_method) {
#ifdef __x86_64__
case PextMethod::kPextInstruction:
CopyNonNullBitmapImpl<PextInstruction>(
non_null_bitmap, sel_bitmap, dst_idx, n_rows, dst_non_null_bitmap);
break;
case PextMethod::kClmul:
CopyNonNullBitmapImpl<PextZp7Clmul>(
non_null_bitmap, sel_bitmap, dst_idx, n_rows, dst_non_null_bitmap);
break;
#endif
case PextMethod::kSimple:
CopyNonNullBitmapImpl<PextZp7Simple>(
non_null_bitmap, sel_bitmap, dst_idx, n_rows, dst_non_null_bitmap);
break;
default:
__builtin_unreachable();
}
}
////////////////////////////////////////////////////////////
// CopySelectedRows
////////////////////////////////////////////////////////////
namespace {
const bool kHasAvx2 = base::CPU().has_avx2();
// Return the number of rows copied through an AVX-optimized implementation.
// These implementations leave a "tail" of non-vectorizable rows that get
// handled by the scalar implementation.
template<int sizeof_type>
int CopySelectedRowsAvx(
const uint16_t* __restrict__ /* sel_rows */,
int /* n_sel_rows */,
const uint8_t* __restrict__ /* src_buf */,
uint8_t* __restrict__ /* dst_buf */) {
return 0;
}
// Define AVX2-optimized variants where possible.
// These are disabled on GCC4 because it doesn't support per-function
// enabling of intrinsics.
#if __x86_64__ && (defined(__clang__) || (defined(__GNUC__) && __GNUC__ >= 5))
template<>
__attribute__((target("avx2")))
int CopySelectedRowsAvx<4>(
const uint16_t* __restrict__ sel_rows,
int n_sel_rows,
const uint8_t* __restrict__ src_buf,
uint8_t* __restrict__ dst_buf) {
static constexpr int sizeof_type = 4;
static constexpr int ints_per_vector = sizeof(__m256i)/sizeof_type;
int iters = n_sel_rows / ints_per_vector;
while (iters--) {
// Load 8x16-bit indexes from sel_rows, zero-extending them to 8x32-bit integers
// since the 'gather' instructions don't support 16-bit indexes.
__m256i indexes = _mm256_cvtepu16_epi32(*reinterpret_cast<const __m128i*>(sel_rows));
// Gather 8x32-bit elements from src_buf[index*sizeof_type] for each index.
// We need this cast to compile on some versions of GCC.
const auto* src_i32 = reinterpret_cast<const int32_t*>(src_buf);
__m256i elems = _mm256_i32gather_epi32(src_i32, indexes, sizeof_type);
// Store the 8x32-bit elements into the destination.
_mm256_storeu_si256(reinterpret_cast<__m256i*>(dst_buf), elems);
dst_buf += ints_per_vector * sizeof_type;
sel_rows += ints_per_vector;
}
return KUDU_ALIGN_DOWN(n_sel_rows, ints_per_vector);
}
template<>
__attribute__((target("avx2")))
int CopySelectedRowsAvx<8>(
const uint16_t* __restrict__ sel_rows,
int n_sel_rows,
const uint8_t* __restrict__ src_buf,
uint8_t* __restrict__ dst_buf) {
static constexpr int sizeof_type = 8;
static constexpr int ints_per_vector = sizeof(__m256i)/sizeof_type;
int iters = n_sel_rows / ints_per_vector;
while (iters--) {
// Load 4x16-bit indexes from sel_rows into 'indexes'. This compiles down
// into a single vpmovzxwd instruction despite looking like four separate loads.
__m128i indexes = _mm_set_epi32(sel_rows[3],
sel_rows[2],
sel_rows[1],
sel_rows[0]);
// Load 4x64-bit integers from src_buf[index * sizeof_type] for each index.
const auto* src_lli = reinterpret_cast<const long long int*>(src_buf); // NOLINT(*)
__m256i elems = _mm256_i32gather_epi64(src_lli, indexes, sizeof_type);
// Store the 4x64-bit integers in the destination.
_mm256_storeu_si256(reinterpret_cast<__m256i*>(dst_buf), elems);
dst_buf += ints_per_vector * sizeof_type;
sel_rows += ints_per_vector;
}
return KUDU_ALIGN_DOWN(n_sel_rows, ints_per_vector);
}
#endif
template<int sizeof_type>
ATTRIBUTE_NOINLINE
void CopySelectedRowsImpl(const uint16_t* __restrict__ sel_rows,
int n_sel_rows,
const uint8_t* __restrict__ src_buf,
uint8_t* __restrict__ dst_buf) {
int rem = n_sel_rows;
if (kHasAvx2) {
int copied = CopySelectedRowsAvx<sizeof_type>(sel_rows, n_sel_rows, src_buf, dst_buf);
rem -= copied;
dst_buf += copied * sizeof_type;
sel_rows += copied;
}
while (rem--) {
int idx = *sel_rows++;
memcpy(dst_buf, src_buf + idx * sizeof_type, sizeof_type);
dst_buf += sizeof_type;
}
// TODO(todd): should we zero out nulls first or otherwise avoid
// copying them?
}
template<int sizeof_type>
ATTRIBUTE_NOINLINE
void CopySelectedRowsImpl(const vector<uint16_t>& sel_rows,
const uint8_t* __restrict__ src_buf,
uint8_t* __restrict__ dst_buf) {
CopySelectedRowsImpl<sizeof_type>(sel_rows.data(), sel_rows.size(), src_buf, dst_buf);
}
} // anonymous namespace
// Copy the selected cells from the column data 'src_buf' into 'dst_buf' as indicated by
// the indices in 'sel_rows'. 'sizeof_type' is the size in bytes of each cell.
void CopySelectedRows(const vector<uint16_t>& sel_rows,
int sizeof_type,
const uint8_t* __restrict__ src_buf,
uint8_t* __restrict__ dst_buf) {
switch (sizeof_type) {
#define CASE(size) \
case size: \
CopySelectedRowsImpl<size>(sel_rows, src_buf, dst_buf); \
break;
CASE(1);
CASE(2);
CASE(4);
CASE(8);
CASE(16);
#undef CASE
default:
LOG(FATAL) << "unexpected type size: " << sizeof_type;
}
}
namespace {
// Specialized division for the known type sizes. Despite having some branching here,
// this is faster than a 'div' instruction which has a 20+ cycle latency.
size_t div_sizeof_type(size_t s, size_t divisor) {
switch (divisor) {
case 1: return s;
case 2: return s/2;
case 4: return s/4;
case 8: return s/8;
case 16: return s/16;
default: return s/divisor;
}
}
// Copy the selected primitive cells (and non-null-bitmap bits) from 'cblock' into 'dst'
// according to the given 'sel_rows'.
void CopySelectedCellsFromColumn(const ColumnBlock& cblock,
const SelectedRows& sel_rows,
ColumnarSerializedBatch::Column* dst) {
DCHECK(cblock.type_info()->physical_type() != BINARY);
size_t sizeof_type = cblock.type_info()->size();
int n_sel = sel_rows.num_selected();
// Number of initial rows in the dst values and null_bitmap.
DCHECK_EQ(dst->data.size() % sizeof_type, 0);
size_t initial_rows = div_sizeof_type(dst->data.size(), sizeof_type);
size_t new_num_rows = initial_rows + n_sel;
dst->data.resize_with_extra_capacity(sizeof_type * new_num_rows);
uint8_t* dst_buf = dst->data.data() + sizeof_type * initial_rows;
const uint8_t* src_buf = cblock.cell_ptr(0);
if (sel_rows.all_selected()) {
memcpy(dst_buf, src_buf, sizeof_type * n_sel);
} else {
CopySelectedRows(sel_rows.indexes(), sizeof_type, src_buf, dst_buf);
}
if (cblock.is_nullable()) {
DCHECK_EQ(dst->non_null_bitmap->size(), BitmapSize(initial_rows));
dst->non_null_bitmap->resize_with_extra_capacity(BitmapSize(new_num_rows));
CopyNonNullBitmap(cblock.non_null_bitmap(),
sel_rows.bitmap(),
initial_rows, cblock.nrows(),
dst->non_null_bitmap->data());
ZeroNullValues(sizeof_type, initial_rows, n_sel,
dst->data.data(), dst->non_null_bitmap->data());
}
}
// For each of the Slices in 'cells_buf', copy the pointed-to data into 'varlen' and
// write the _end_ offset of the copied data into 'offsets_out'. This assumes (and
// DCHECKs) that the _start_ offset of each cell was already previously written by a
// previous invocation of this function.
void CopySlicesAndWriteEndOffsets(const Slice* __restrict__ cells_buf,
const SelectedRows& sel_rows,
uint32_t* __restrict__ offsets_out,
faststring* varlen) {
const Slice* cell_slices = reinterpret_cast<const Slice*>(cells_buf);
size_t total_added_size = 0;
sel_rows.ForEachIndex(
[&](uint16_t i) {
total_added_size += cell_slices[i].size();
});
// The output array should already have an entry for the start offset
// of our first cell.
DCHECK_EQ(offsets_out[-1], varlen->size());
int old_size = varlen->size();
varlen->resize_with_extra_capacity(old_size + total_added_size);
uint8_t* dst_base = varlen->data();
uint8_t* dst = dst_base + old_size;
sel_rows.ForEachIndex(
[&](uint16_t i) {
const Slice* s = &cell_slices[i];
if (!s->empty()) {
strings::memcpy_inlined(dst, s->data(), s->size());
}
dst += s->size();
*offsets_out++ = dst - dst_base;
});
}
// Copy variable-length cells into 'dst' using an Arrow-style serialization:
// a list of offsets in the 'data' array and the data itself in the 'varlen_data'
// array.
//
// The offset array has a length one greater than the number of cells.
void CopySelectedVarlenCellsFromColumn(const ColumnBlock& cblock,
const SelectedRows& sel_rows,
ColumnarSerializedBatch::Column* dst) {
using offset_type = uint32_t;
DCHECK(cblock.type_info()->physical_type() == BINARY);
int n_sel = sel_rows.num_selected();
DCHECK_GT(n_sel, 0);
// If this is the first call, append a '0' entry for the offset of the first string.
if (dst->data.size() == 0) {
CHECK_EQ(dst->varlen_data->size(), 0);
offset_type zero_offset = 0;
dst->data.append(&zero_offset, sizeof(zero_offset));
}
// Number of initial rows in the dst values and null_bitmap.
DCHECK_EQ(dst->data.size() % sizeof(offset_type), 0);
size_t initial_offset_count = div_sizeof_type(dst->data.size(), sizeof(offset_type));
size_t initial_rows = initial_offset_count - 1;
size_t new_offset_count = initial_offset_count + n_sel;
size_t new_num_rows = initial_rows + n_sel;
if (cblock.is_nullable()) {
DCHECK_EQ(dst->non_null_bitmap->size(), BitmapSize(initial_rows));
dst->non_null_bitmap->resize_with_extra_capacity(BitmapSize(new_num_rows));
CopyNonNullBitmap(cblock.non_null_bitmap(),
sel_rows.bitmap(),
initial_rows, cblock.nrows(),
dst->non_null_bitmap->data());
ZeroNullValues(sizeof(Slice), 0, cblock.nrows(),
const_cast<ColumnBlock&>(cblock).data(), cblock.non_null_bitmap());
}
dst->data.resize_with_extra_capacity(sizeof(offset_type) * new_offset_count);
offset_type* dst_offset = reinterpret_cast<offset_type*>(dst->data.data()) + initial_offset_count;
const Slice* src_slices = reinterpret_cast<const Slice*>(cblock.cell_ptr(0));
CopySlicesAndWriteEndOffsets(src_slices, sel_rows, dst_offset,
dst->varlen_data ? &(*dst->varlen_data) : nullptr);
}
} // anonymous namespace
} // namespace internal
ColumnarSerializedBatch::ColumnarSerializedBatch(const Schema& rowblock_schema,
const Schema& client_schema,
int expected_batch_size_bytes) {
// Initialize buffers for the columns.
int64_t row_bytes = client_schema.byte_size();
columns_.reserve(client_schema.num_columns());
for (const auto& schema_col : client_schema.columns()) {
columns_.emplace_back();
auto& col = columns_.back();
col.rowblock_schema_col_idx = rowblock_schema.find_column(schema_col.name());
CHECK_NE(col.rowblock_schema_col_idx, -1);
// Size the initial buffer based on the percentage of the total row that this column
// takes up. This isn't fully accurate because of costs like the null bitmap or varlen
// data, but tries to reasonably apportion the memory budget across the columns.
col.data.reserve(schema_col.type_info()->size() * expected_batch_size_bytes / row_bytes);
if (schema_col.type_info()->physical_type() == BINARY) {
col.varlen_data.emplace();
}
if (schema_col.is_nullable()) {
col.non_null_bitmap.emplace();
}
}
}
int ColumnarSerializedBatch::AddRowBlock(const RowBlock& block) {
DCHECK_GT(block.nrows(), 0);
SelectedRows sel = block.selection_vector()->GetSelectedRows();
if (sel.num_selected() == 0) {
return 0;
}
int col_idx = 0;
for (const auto& col : columns_) {
const ColumnBlock& column_block = block.column_block(col.rowblock_schema_col_idx);
if (column_block.type_info()->physical_type() == BINARY) {
internal::CopySelectedVarlenCellsFromColumn(
column_block,
sel,
&columns_[col_idx]);
} else {
internal::CopySelectedCellsFromColumn(
column_block,
sel,
&columns_[col_idx]);
}
col_idx++;
}
return sel.num_selected();
}
} // namespace kudu