/*! * Copyright (c) 2021 Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. * * Author: Alberto Ferreira */ #ifndef LIGHTGBM_UTILS_CHUNKED_ARRAY_HPP_ #define LIGHTGBM_UTILS_CHUNKED_ARRAY_HPP_ #include #include #include #include #include namespace LightGBM { /** * Container that manages a dynamic array of fixed-length chunks. * * The class also takes care of allocation & release of the underlying * memory. It can be used with either a high or low-level API. * * The high-level API allocates chunks as needed, manages addresses automatically and keeps * track of number of inserted elements, but is not thread-safe (this is ok as usually input is a streaming iterator). * For parallel input sources the low-level API must be used. * * Note: When using this for `LGBM_DatasetCreateFromMats` use a * chunk_size multiple of #num_cols for your dataset, so each chunk * contains "complete" instances. * * === High-level insert API intro === * * The easiest way to use is: * 0. ChunkedArray(chunk_size) # Choose appropriate size * 1. add(value) # as many times as you want (will generate chunks as needed) * 2. data() or void_data() # retrieves a T** or void** pointer (useful for `LGBM_DatasetCreateFromMats`). * * Useful query methods (all O(1)): * - get_add_count() # total count of added elements. * - get_chunks_count() # how many chunks are currently allocated. * - get_current_chunk_added_count() # for the last add() chunk, how many items there are. * - get_chunk_size() # get constant chunk_size from constructor call. * * With those you can generate int32_t sizes[]. Last chunk can be smaller than chunk_size, so, for any i: * - sizes[i class ChunkedArray { public: explicit ChunkedArray(size_t chunk_size) : _chunk_size(chunk_size), _last_chunk_idx(0), _last_idx_in_last_chunk(0) { if (chunk_size == 0) { Log::Fatal("ChunkedArray chunk size must be larger than 0!"); } new_chunk(); } ~ChunkedArray() { release(); } /** * Adds a value to the chunks sequentially. * If the last chunk is full it creates a new one and appends to it. * * @param value value to insert. */ void add(T value) { if (!within_bounds(_last_chunk_idx, _last_idx_in_last_chunk)) { new_chunk(); ++_last_chunk_idx; _last_idx_in_last_chunk = 0; } CHECK_EQ(setitem(_last_chunk_idx, _last_idx_in_last_chunk, value), 0); ++_last_idx_in_last_chunk; } /** * @return Number of add() calls. */ size_t get_add_count() const { return _last_chunk_idx * _chunk_size + _last_idx_in_last_chunk; } /** * @return Number of allocated chunks. */ size_t get_chunks_count() const { return _chunks.size(); } /** * @return Number of elemends add()'ed in the last chunk. */ size_t get_last_chunk_add_count() const { return _last_idx_in_last_chunk; } /** * Getter for the chunk size set at the constructor. * * @return Return the size of chunks. */ size_t get_chunk_size() const { return _chunk_size; } /** * Returns the pointer to the raw chunks data. * * @return T** pointer to raw data. */ T **data() noexcept { return _chunks.data(); } /** * Returns the pointer to the raw chunks data, but cast to void**. * This is so ``LGBM_DatasetCreateFromMats`` accepts it. * * @return void** pointer to raw data. */ void **data_as_void() noexcept { return reinterpret_cast(_chunks.data()); } /** * Coalesces (copies chunked data) to a contiguous array of the same type. * It assumes that ``other`` has enough space to receive that data. * * @param other array with elements T of size >= this->get_add_count(). * @param all_valid_addresses * If true exports values from all valid addresses independently of add() count. * Otherwise, exports only up to `get_add_count()` addresses. */ void coalesce_to(T *other, bool all_valid_addresses = false) const { const size_t full_chunks = this->get_chunks_count() - 1; // Copy full chunks: size_t i = 0; for (size_t chunk = 0; chunk < full_chunks; ++chunk) { T* chunk_ptr = _chunks[chunk]; for (size_t in_chunk_idx = 0; in_chunk_idx < _chunk_size; ++in_chunk_idx) { other[i++] = chunk_ptr[in_chunk_idx]; } } // Copy filled values from last chunk only: const size_t last_chunk_elems_to_copy = all_valid_addresses ? _chunk_size : this->get_last_chunk_add_count(); T* chunk_ptr = _chunks[full_chunks]; for (size_t in_chunk_idx = 0; in_chunk_idx < last_chunk_elems_to_copy; ++in_chunk_idx) { other[i++] = chunk_ptr[in_chunk_idx]; } } /** * Return value from array of chunks. * * @param chunk_index index of the chunk * @param index_within_chunk index within chunk * @param on_fail_value sentinel value. If out of bounds returns that value. * * @return pointer or nullptr if index is out of bounds. */ T getitem(size_t chunk_index, size_t index_within_chunk, T on_fail_value) const noexcept { if (within_bounds(chunk_index, index_within_chunk)) return _chunks[chunk_index][index_within_chunk]; else return on_fail_value; } /** * Sets the value at a specific address in one of the chunks. * * @param chunk_index index of the chunk * @param index_within_chunk index within chunk * @param value value to store * * @return 0 = success, -1 = out of bounds access. */ int setitem(size_t chunk_index, size_t index_within_chunk, T value) noexcept { if (within_bounds(chunk_index, index_within_chunk)) { _chunks[chunk_index][index_within_chunk] = value; return 0; } else { return -1; } } /** * To reset storage call this. * Will release existing resources and prepare for reuse. */ void clear() noexcept { release(); new_chunk(); } /** * Deletes all the allocated chunks. * Do not use container after this! See ``clear()`` instead. */ void release() noexcept { std::for_each(_chunks.begin(), _chunks.end(), [](T* c) { delete[] c; }); _chunks.clear(); _chunks.shrink_to_fit(); _last_chunk_idx = 0; _last_idx_in_last_chunk = 0; } /** * As the array is dynamic, checks whether a given address is currently within bounds. * * @param chunk_index index of the chunk * @param index_within_chunk index within that chunk * @return true if that chunk is already allocated and index_within_chunk < chunk size. */ inline bool within_bounds(size_t chunk_index, size_t index_within_chunk) const { return (chunk_index < _chunks.size()) && (index_within_chunk < _chunk_size); } /** * Adds a new chunk to the array of chunks. Not thread-safe. */ void new_chunk() { _chunks.push_back(new (std::nothrow) T[_chunk_size]); // Check memory allocation success: if (!_chunks[_chunks.size() - 1]) { release(); Log::Fatal("Memory exhausted! Cannot allocate new ChunkedArray chunk."); } } private: const size_t _chunk_size; std::vector _chunks; // For the add() interface & some of the get_*() queries: size_t _last_chunk_idx; //