BLI: add chunked list data structure that uses linear allocator
This adds a new special purpose container data structure that can be used to gather many elements into many (potentially small) lists efficiently. I originally worked on this data structure because I might want to use it in #118772. However, also it's useful in the geometry nodes logger already. I'm measuring a 10-20% speed improvement in my many-math-nodes file when I enable logging for all sockets (not just the ones that are currently visible). Pull Request: https://projects.blender.org/blender/blender/pulls/118774
This commit is contained in:
190
source/blender/blenlib/BLI_linear_allocator_chunked_list.hh
Normal file
190
source/blender/blenlib/BLI_linear_allocator_chunked_list.hh
Normal file
@@ -0,0 +1,190 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "BLI_linear_allocator.hh"
|
||||
#include "BLI_struct_equality_utils.hh"
|
||||
#include "BLI_utility_mixins.hh"
|
||||
|
||||
namespace blender::linear_allocator {
|
||||
|
||||
/**
|
||||
* The list is a linked list of segments containing multiple elements. The capacity of each segment
|
||||
* is a template parameter because that removes the need to store it for every segment.
|
||||
*/
|
||||
template<typename T, int64_t Capacity> struct ChunkedListSegment {
|
||||
/** Pointer to the next segment in the list. */
|
||||
ChunkedListSegment *next = nullptr;
|
||||
/**
|
||||
* Number of constructed elements in this segment. The constructed elements are always at the
|
||||
* beginning of the array below.
|
||||
*/
|
||||
int64_t size = 0;
|
||||
/**
|
||||
* The memory that actually contains the values in the end. The values are constructed and
|
||||
* destructed by higher level code.
|
||||
*/
|
||||
std::array<TypedBuffer<T>, Capacity> values;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a special purpose container data structure that can be used to efficiently gather many
|
||||
* elements into many (small) lists for later retrieval. Insertion order is *not* maintained.
|
||||
*
|
||||
* To use this data structure, one has to have a separate #LinearAllocator which is passed to the
|
||||
* `append` function. This allows the same allocator to be used by many lists. Passing it into the
|
||||
* append function also removes the need to store the allocator pointer in every list.
|
||||
*
|
||||
* It is an improvement over #Vector because it does not require any reallocations. #VectorList
|
||||
* could also be used to overcome the reallocation issue.
|
||||
*
|
||||
* This data structure is also an improvement over #VectorList because:
|
||||
* - It has a much lower memory footprint when empty.
|
||||
* - Allows using a #LinearAllocator for all allocations, without storing the pointer to it in
|
||||
* every vector.
|
||||
* - It wastes less memory due to over-allocations.
|
||||
*/
|
||||
template<typename T, int64_t SegmentCapacity = 4> class ChunkedList : NonCopyable {
|
||||
private:
|
||||
using Segment = ChunkedListSegment<T, SegmentCapacity>;
|
||||
Segment *current_segment_ = nullptr;
|
||||
|
||||
public:
|
||||
ChunkedList() = default;
|
||||
|
||||
ChunkedList(ChunkedList &&other)
|
||||
{
|
||||
current_segment_ = other.current_segment_;
|
||||
other.current_segment_ = nullptr;
|
||||
}
|
||||
|
||||
~ChunkedList()
|
||||
{
|
||||
/* This code assumes that the #ChunkedListSegment does not have to be destructed if the
|
||||
* contained type is trivially destructible. */
|
||||
static_assert(std::is_trivially_destructible_v<ChunkedListSegment<int, 4>>);
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
for (Segment *segment = current_segment_; segment; segment = segment->next) {
|
||||
for (const int64_t i : IndexRange(segment->size)) {
|
||||
T &value = *segment->values[i];
|
||||
std::destroy_at(&value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChunkedList &operator=(ChunkedList &&other)
|
||||
{
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
}
|
||||
std::destroy_at(this);
|
||||
new (this) ChunkedList(std::move(other));
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an element to the list. The insertion order is not maintained. The given allocator is used
|
||||
* to allocate any extra memory that may be needed.
|
||||
*/
|
||||
void append(LinearAllocator<> &allocator, const T &value)
|
||||
{
|
||||
this->append_as(allocator, value);
|
||||
}
|
||||
|
||||
void append(LinearAllocator<> &allocator, T &&value)
|
||||
{
|
||||
this->append_as(allocator, std::move(value));
|
||||
}
|
||||
|
||||
template<typename... Args> void append_as(LinearAllocator<> &allocator, Args &&...args)
|
||||
{
|
||||
if (current_segment_ == nullptr || current_segment_->size == SegmentCapacity) {
|
||||
/* Allocate a new segment if necessary. */
|
||||
static_assert(std::is_trivially_destructible_v<Segment>);
|
||||
Segment *new_segment = allocator.construct<Segment>().release();
|
||||
new_segment->next = current_segment_;
|
||||
current_segment_ = new_segment;
|
||||
}
|
||||
T *value = &*current_segment_->values[current_segment_->size++];
|
||||
new (value) T(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
class ConstIterator {
|
||||
private:
|
||||
const Segment *segment_ = nullptr;
|
||||
int64_t index_ = 0;
|
||||
|
||||
public:
|
||||
ConstIterator(const Segment *segment, int64_t index = 0) : segment_(segment), index_(index) {}
|
||||
|
||||
ConstIterator &operator++()
|
||||
{
|
||||
index_++;
|
||||
if (index_ == segment_->size) {
|
||||
segment_ = segment_->next;
|
||||
index_ = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
const T &operator*() const
|
||||
{
|
||||
return *segment_->values[index_];
|
||||
}
|
||||
|
||||
BLI_STRUCT_EQUALITY_OPERATORS_2(ConstIterator, segment_, index_)
|
||||
};
|
||||
|
||||
class MutableIterator {
|
||||
private:
|
||||
Segment *segment_ = nullptr;
|
||||
int64_t index_ = 0;
|
||||
|
||||
public:
|
||||
MutableIterator(Segment *segment, int64_t index = 0) : segment_(segment), index_(index) {}
|
||||
|
||||
MutableIterator &operator++()
|
||||
{
|
||||
index_++;
|
||||
if (index_ == segment_->size) {
|
||||
segment_ = segment_->next;
|
||||
index_ = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
T &operator*()
|
||||
{
|
||||
return *segment_->values[index_];
|
||||
}
|
||||
|
||||
BLI_STRUCT_EQUALITY_OPERATORS_2(MutableIterator, segment_, index_)
|
||||
};
|
||||
|
||||
ConstIterator begin() const
|
||||
{
|
||||
return ConstIterator(current_segment_, 0);
|
||||
}
|
||||
|
||||
ConstIterator end() const
|
||||
{
|
||||
return ConstIterator(nullptr, 0);
|
||||
}
|
||||
|
||||
MutableIterator begin()
|
||||
{
|
||||
return MutableIterator(current_segment_, 0);
|
||||
}
|
||||
|
||||
MutableIterator end()
|
||||
{
|
||||
return MutableIterator(nullptr, 0);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender::linear_allocator
|
||||
@@ -263,6 +263,7 @@ set(SRC
|
||||
BLI_lazy_threading.hh
|
||||
BLI_length_parameterize.hh
|
||||
BLI_linear_allocator.hh
|
||||
BLI_linear_allocator_chunked_list.hh
|
||||
BLI_link_utils.h
|
||||
BLI_linklist.h
|
||||
BLI_linklist_lockfree.h
|
||||
@@ -521,6 +522,7 @@ if(WITH_GTESTS)
|
||||
tests/BLI_kdtree_test.cc
|
||||
tests/BLI_length_parameterize_test.cc
|
||||
tests/BLI_linear_allocator_test.cc
|
||||
tests/BLI_linear_allocator_chunked_list_test.cc
|
||||
tests/BLI_linklist_lockfree_test.cc
|
||||
tests/BLI_listbase_test.cc
|
||||
tests/BLI_map_test.cc
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 */
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "BLI_linear_allocator_chunked_list.hh"
|
||||
#include "BLI_set.hh"
|
||||
|
||||
#include "BLI_strict_flags.h" /* Keep last. */
|
||||
|
||||
namespace blender::linear_allocator::tests {
|
||||
|
||||
TEST(LinearAllocator_ChunkedList, Append)
|
||||
{
|
||||
LinearAllocator<> allocator;
|
||||
ChunkedList<std::string> list;
|
||||
|
||||
list.append(allocator, "1");
|
||||
list.append(allocator, "2");
|
||||
list.append(allocator, "this_is_an_extra_long_string");
|
||||
|
||||
Set<std::string> retrieved_values;
|
||||
for (const std::string &value : const_cast<const ChunkedList<std::string> &>(list)) {
|
||||
retrieved_values.add(value);
|
||||
}
|
||||
EXPECT_EQ(retrieved_values.size(), 3);
|
||||
EXPECT_TRUE(retrieved_values.contains("1"));
|
||||
EXPECT_TRUE(retrieved_values.contains("2"));
|
||||
EXPECT_TRUE(retrieved_values.contains("this_is_an_extra_long_string"));
|
||||
}
|
||||
|
||||
TEST(LinearAllocator_ChunkedList, AppendMany)
|
||||
{
|
||||
LinearAllocator<> allocator;
|
||||
ChunkedList<int> list;
|
||||
|
||||
for (const int64_t i : IndexRange(10000)) {
|
||||
list.append(allocator, int(i));
|
||||
}
|
||||
|
||||
Set<int> values;
|
||||
for (const int value : list) {
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
EXPECT_EQ(values.size(), 10000);
|
||||
}
|
||||
|
||||
TEST(LinearAllocator_ChunkedList, Move)
|
||||
{
|
||||
LinearAllocator<> allocator;
|
||||
ChunkedList<int> a;
|
||||
a.append(allocator, 1);
|
||||
ChunkedList<int> b = std::move(a);
|
||||
|
||||
a.append(allocator, 2);
|
||||
b.append(allocator, 3);
|
||||
|
||||
{
|
||||
Set<int> a_values;
|
||||
for (const int value : a) {
|
||||
a_values.add(value);
|
||||
}
|
||||
Set<int> b_values;
|
||||
for (const int value : b) {
|
||||
b_values.add(value);
|
||||
}
|
||||
|
||||
EXPECT_EQ(a_values.size(), 1);
|
||||
EXPECT_TRUE(a_values.contains(2));
|
||||
|
||||
EXPECT_EQ(b_values.size(), 2);
|
||||
EXPECT_TRUE(b_values.contains(1));
|
||||
EXPECT_TRUE(b_values.contains(3));
|
||||
}
|
||||
|
||||
a = std::move(b);
|
||||
/* Want to test self-move. Using std::move twice quiets a compiler warning. */
|
||||
a = std::move(std::move(a));
|
||||
|
||||
{
|
||||
Set<int> a_values;
|
||||
for (const int value : a) {
|
||||
a_values.add(value);
|
||||
}
|
||||
Set<int> b_values;
|
||||
for (const int value : b) {
|
||||
b_values.add(value);
|
||||
}
|
||||
|
||||
EXPECT_EQ(a_values.size(), 2);
|
||||
EXPECT_TRUE(a_values.contains(1));
|
||||
EXPECT_TRUE(a_values.contains(3));
|
||||
|
||||
EXPECT_TRUE(b_values.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::linear_allocator::tests
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "BLI_compute_context.hh"
|
||||
#include "BLI_enumerable_thread_specific.hh"
|
||||
#include "BLI_generic_pointer.hh"
|
||||
#include "BLI_linear_allocator_chunked_list.hh"
|
||||
#include "BLI_multi_value_map.hh"
|
||||
|
||||
#include "BKE_geometry_set.hh"
|
||||
@@ -207,13 +208,13 @@ class GeoTreeLogger {
|
||||
StringRefNull message;
|
||||
};
|
||||
|
||||
Vector<WarningWithNode> node_warnings;
|
||||
Vector<SocketValueLog> input_socket_values;
|
||||
Vector<SocketValueLog> output_socket_values;
|
||||
Vector<NodeExecutionTime> node_execution_times;
|
||||
Vector<ViewerNodeLogWithNode, 0> viewer_node_logs;
|
||||
Vector<AttributeUsageWithNode, 0> used_named_attributes;
|
||||
Vector<DebugMessage, 0> debug_messages;
|
||||
linear_allocator::ChunkedList<WarningWithNode> node_warnings;
|
||||
linear_allocator::ChunkedList<SocketValueLog, 16> input_socket_values;
|
||||
linear_allocator::ChunkedList<SocketValueLog, 16> output_socket_values;
|
||||
linear_allocator::ChunkedList<NodeExecutionTime, 16> node_execution_times;
|
||||
linear_allocator::ChunkedList<ViewerNodeLogWithNode> viewer_node_logs;
|
||||
linear_allocator::ChunkedList<AttributeUsageWithNode> used_named_attributes;
|
||||
linear_allocator::ChunkedList<DebugMessage> debug_messages;
|
||||
|
||||
GeoTreeLogger();
|
||||
~GeoTreeLogger();
|
||||
|
||||
@@ -217,7 +217,7 @@ class LazyFunctionForBakeNode final : public LazyFunction {
|
||||
user_data))
|
||||
{
|
||||
tree_logger->node_warnings.append(
|
||||
{node_.identifier, {NodeWarningType::Error, info->message}});
|
||||
*tree_logger->allocator, {node_.identifier, {NodeWarningType::Error, info->message}});
|
||||
}
|
||||
this->set_default_outputs(params);
|
||||
}
|
||||
|
||||
@@ -330,7 +330,8 @@ class LazyFunctionForGeometryNode : public LazyFunction {
|
||||
|
||||
if (geo_eval_log::GeoTreeLogger *tree_logger = local_user_data.try_get_tree_logger(*user_data))
|
||||
{
|
||||
tree_logger->node_execution_times.append({node_.identifier, start_time, end_time});
|
||||
tree_logger->node_execution_times.append(*tree_logger->allocator,
|
||||
{node_.identifier, start_time, end_time});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1706,6 +1707,7 @@ class LazyFunctionForRepeatZone : public LazyFunction {
|
||||
user_data))
|
||||
{
|
||||
tree_logger->node_warnings.append(
|
||||
*tree_logger->allocator,
|
||||
{repeat_output_bnode_.identifier,
|
||||
{NodeWarningType::Info, N_("Inspection index is out of range")}});
|
||||
}
|
||||
@@ -2004,7 +2006,8 @@ class GeometryNodesLazyFunctionLogger : public lf::GraphExecutor::Logger {
|
||||
if (!bsockets.is_empty()) {
|
||||
const bNodeSocket &bsocket = *bsockets[0];
|
||||
const bNode &bnode = bsocket.owner_node();
|
||||
tree_logger->debug_messages.append({bnode.identifier, thread_id_str});
|
||||
tree_logger->debug_messages.append(*tree_logger->allocator,
|
||||
{bnode.identifier, thread_id_str});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,8 @@ void GeoTreeLogger::log_value(const bNode &node, const bNodeSocket &socket, cons
|
||||
auto store_logged_value = [&](destruct_ptr<ValueLog> value_log) {
|
||||
auto &socket_values = socket.in_out == SOCK_IN ? this->input_socket_values :
|
||||
this->output_socket_values;
|
||||
socket_values.append({node.identifier, socket.index(), std::move(value_log)});
|
||||
socket_values.append(*this->allocator,
|
||||
{node.identifier, socket.index(), std::move(value_log)});
|
||||
};
|
||||
|
||||
auto log_generic_value = [&](const CPPType &type, const void *value) {
|
||||
@@ -225,7 +226,7 @@ void GeoTreeLogger::log_viewer_node(const bNode &viewer_node, bke::GeometrySet g
|
||||
destruct_ptr<ViewerNodeLog> log = this->allocator->construct<ViewerNodeLog>();
|
||||
log->geometry = std::move(geometry);
|
||||
log->geometry.ensure_owns_direct_data();
|
||||
this->viewer_node_logs.append({viewer_node.identifier, std::move(log)});
|
||||
this->viewer_node_logs.append(*this->allocator, {viewer_node.identifier, std::move(log)});
|
||||
}
|
||||
|
||||
void GeoTreeLog::ensure_node_warnings()
|
||||
|
||||
@@ -22,6 +22,7 @@ void GeoNodeExecParams::error_message_add(const NodeWarningType type,
|
||||
{
|
||||
if (geo_eval_log::GeoTreeLogger *tree_logger = this->get_local_tree_logger()) {
|
||||
tree_logger->node_warnings.append(
|
||||
*tree_logger->allocator,
|
||||
{node_.identifier, {type, tree_logger->allocator->copy_string(message)}});
|
||||
}
|
||||
}
|
||||
@@ -31,6 +32,7 @@ void GeoNodeExecParams::used_named_attribute(const StringRef attribute_name,
|
||||
{
|
||||
if (geo_eval_log::GeoTreeLogger *tree_logger = this->get_local_tree_logger()) {
|
||||
tree_logger->used_named_attributes.append(
|
||||
*tree_logger->allocator,
|
||||
{node_.identifier, tree_logger->allocator->copy_string(attribute_name), usage});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user