mirror of
https://github.com/NixOS/nix.git
synced 2025-11-09 03:56:01 +01:00
libexpr: Structural sharing of attrsets
This changes the implementation of Bindings to allow
for a more space-efficient implementation of attribute
set merges. This is accomplished by "layering" over the "base" Bindings.
The top "layer" is naturally the right-hand-side of the update operator //.
Such an implementation leads to significantly better memory usage on
something like nixpkgs:
nix-env --query --available --out-path --file ../nixpkgs --eval-system x86_64-linux > /dev/null
Comparison against 2b0fd88324 for x86_64-linux on nixpkgs f06c7c3b6f5074dbffcf02542fb86af3a5526afa:
| metric | mean_before | mean_after | mean_diff | mean_%_change | p_value | t_stat |
| - | - | - | - | - | - | - |
| cpuTime | 21.1520 | 21.3414 | 0.1894 | 0.7784 | 0.3190 | 1.0219 |
| envs.bytes | 461451951.6190 | 461451951.6190 | - | - | - | - |
| envs.elements | 34344544.8571 | 34344544.8571 | - | - | - | - |
| envs.number | 23336949.0952 | 23336949.0952 | - | - | - | - |
| gc.cycles | 7.5238 | 7.2857 | -0.2381 | -4.6825 | 0.0565 | -2.0244 |
| gc.heapSize | 1777848124.9524 | 1252162023.6190 | -525686101.3333 | -29.9472 | 0.0000 | -8.7041 |
| gc.totalBytes | 3102787383.6190 | 2498431578.6667 | -604355804.9524 | -19.7704 | 0.0000 | -9.3502 |
| list.bytes | 59928225.9048 | 59928225.9048 | - | - | - | - |
| list.concats | 1240028.2857 | 1240028.2857 | - | - | - | - |
| list.elements | 7491028.2381 | 7491028.2381 | - | - | - | - |
| nrAvoided | 28165342.2381 | 28165342.2381 | - | - | - | - |
| nrExprs | 1577412.9524 | 1577412.9524 | - | - | - | - |
| nrFunctionCalls | 20970743.4286 | 20970743.4286 | - | - | - | - |
| nrLookups | 10867306.0952 | 10867306.0952 | - | - | - | - |
| nrOpUpdateValuesCopied | 61206062.0000 | 25748169.5238 | -35457892.4762 | -58.8145 | 0.0000 | -8.9189 |
| nrOpUpdates | 2167097.4286 | 2167097.4286 | - | - | - | - |
| nrPrimOpCalls | 12337423.4286 | 12337423.4286 | - | - | - | - |
| nrThunks | 29361806.7619 | 29361806.7619 | - | - | - | - |
| sets.bytes | 1393822818.6667 | 897587655.2381 | -496235163.4286 | -36.7168 | 0.0000 | -9.1115 |
| sets.elements | 84504465.3333 | 48270845.9524 | -36233619.3810 | -43.8698 | 0.0000 | -8.9181 |
| sets.number | 5218921.6667 | 5218921.6667 | - | - | - | - |
| sizes.Attr | 16.0000 | 16.0000 | - | - | - | - |
| sizes.Bindings | 8.0000 | 24.0000 | 16.0000 | 200.0000 | - | inf |
| sizes.Env | 8.0000 | 8.0000 | - | - | - | - |
| sizes.Value | 16.0000 | 16.0000 | - | - | - | - |
| symbols.bytes | 1368494.0952 | 1368494.0952 | - | - | - | - |
| symbols.number | 109147.1905 | 109147.1905 | - | - | - | - |
| time.cpu | 21.1520 | 21.3414 | 0.1894 | 0.7784 | 0.3190 | 1.0219 |
| time.gc | 1.6011 | 0.8508 | -0.7503 | -37.1507 | 0.0017 | -3.6328 |
| time.gcFraction | 0.0849 | 0.0399 | -0.0450 | -37.4504 | 0.0035 | -3.3116 |
| values.bytes | 615968144.7619 | 615968144.7619 | - | - | - | - |
| values.number | 38498009.0476 | 38498009.0476 | - | - | - | - |
Overall this does slow down the evaluator slightly (no more than ~10% in most cases),
but this seems like a very decent tradeoff for shaving off 33% of memory usage.
This commit is contained in:
parent
3eb223f4bb
commit
6138bc3de3
7 changed files with 424 additions and 63 deletions
|
|
@ -371,13 +371,24 @@ bool nix_has_attr_byname(nix_c_context * context, const nix_value * value, EvalS
|
||||||
NIXC_CATCH_ERRS_RES(false);
|
NIXC_CATCH_ERRS_RES(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
nix_value * nix_get_attr_byidx(
|
static void collapse_attrset_layer_chain_if_needed(nix::Value & v, EvalState * state)
|
||||||
nix_c_context * context, const nix_value * value, EvalState * state, unsigned int i, const char ** name)
|
{
|
||||||
|
auto & attrs = *v.attrs();
|
||||||
|
if (attrs.isLayered()) {
|
||||||
|
auto bindings = state->state.buildBindings(attrs.size());
|
||||||
|
std::ranges::copy(attrs, std::back_inserter(bindings));
|
||||||
|
v.mkAttrs(bindings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nix_value *
|
||||||
|
nix_get_attr_byidx(nix_c_context * context, nix_value * value, EvalState * state, unsigned int i, const char ** name)
|
||||||
{
|
{
|
||||||
if (context)
|
if (context)
|
||||||
context->last_err_code = NIX_OK;
|
context->last_err_code = NIX_OK;
|
||||||
try {
|
try {
|
||||||
auto & v = check_value_in(value);
|
auto & v = check_value_in(value);
|
||||||
|
collapse_attrset_layer_chain_if_needed(v, state);
|
||||||
const nix::Attr & a = (*v.attrs())[i];
|
const nix::Attr & a = (*v.attrs())[i];
|
||||||
*name = state->state.symbols[a.name].c_str();
|
*name = state->state.symbols[a.name].c_str();
|
||||||
nix_gc_incref(nullptr, a.value);
|
nix_gc_incref(nullptr, a.value);
|
||||||
|
|
@ -387,13 +398,13 @@ nix_value * nix_get_attr_byidx(
|
||||||
NIXC_CATCH_ERRS_NULL
|
NIXC_CATCH_ERRS_NULL
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *
|
const char * nix_get_attr_name_byidx(nix_c_context * context, nix_value * value, EvalState * state, unsigned int i)
|
||||||
nix_get_attr_name_byidx(nix_c_context * context, const nix_value * value, EvalState * state, unsigned int i)
|
|
||||||
{
|
{
|
||||||
if (context)
|
if (context)
|
||||||
context->last_err_code = NIX_OK;
|
context->last_err_code = NIX_OK;
|
||||||
try {
|
try {
|
||||||
auto & v = check_value_in(value);
|
auto & v = check_value_in(value);
|
||||||
|
collapse_attrset_layer_chain_if_needed(v, state);
|
||||||
const nix::Attr & a = (*v.attrs())[i];
|
const nix::Attr & a = (*v.attrs())[i];
|
||||||
return state->state.symbols[a.name].c_str();
|
return state->state.symbols[a.name].c_str();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -297,8 +297,8 @@ bool nix_has_attr_byname(nix_c_context * context, const nix_value * value, EvalS
|
||||||
* @param[out] name will store a pointer to the attribute name
|
* @param[out] name will store a pointer to the attribute name
|
||||||
* @return value, NULL in case of errors
|
* @return value, NULL in case of errors
|
||||||
*/
|
*/
|
||||||
nix_value * nix_get_attr_byidx(
|
nix_value *
|
||||||
nix_c_context * context, const nix_value * value, EvalState * state, unsigned int i, const char ** name);
|
nix_get_attr_byidx(nix_c_context * context, nix_value * value, EvalState * state, unsigned int i, const char ** name);
|
||||||
|
|
||||||
/** @brief Get an attribute name by index in the sorted bindings
|
/** @brief Get an attribute name by index in the sorted bindings
|
||||||
*
|
*
|
||||||
|
|
@ -311,8 +311,7 @@ nix_value * nix_get_attr_byidx(
|
||||||
* @param[in] i attribute index
|
* @param[in] i attribute index
|
||||||
* @return name, NULL in case of errors
|
* @return name, NULL in case of errors
|
||||||
*/
|
*/
|
||||||
const char *
|
const char * nix_get_attr_name_byidx(nix_c_context * context, nix_value * value, EvalState * state, unsigned int i);
|
||||||
nix_get_attr_name_byidx(nix_c_context * context, const nix_value * value, EvalState * state, unsigned int i);
|
|
||||||
|
|
||||||
/**@}*/
|
/**@}*/
|
||||||
/** @name Initializers
|
/** @name Initializers
|
||||||
|
|
|
||||||
|
|
@ -437,4 +437,31 @@ TEST_F(nix_api_expr_test, nix_value_call_multi_no_args)
|
||||||
assert_ctx_ok();
|
assert_ctx_ok();
|
||||||
ASSERT_EQ(3, rInt);
|
ASSERT_EQ(3, rInt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(nix_api_expr_test, nix_expr_attrset_update)
|
||||||
|
{
|
||||||
|
nix_expr_eval_from_string(ctx, state, "{ a = 0; b = 2; } // { a = 1; b = 3; } // { a = 2; }", ".", value);
|
||||||
|
assert_ctx_ok();
|
||||||
|
|
||||||
|
ASSERT_EQ(nix_get_attrs_size(ctx, value), 2);
|
||||||
|
assert_ctx_ok();
|
||||||
|
std::array<std::pair<std::string_view, nix_value *>, 2> values;
|
||||||
|
for (unsigned int i = 0; i < 2; ++i) {
|
||||||
|
const char * name;
|
||||||
|
values[i].second = nix_get_attr_byidx(ctx, value, state, i, &name);
|
||||||
|
assert_ctx_ok();
|
||||||
|
values[i].first = name;
|
||||||
|
}
|
||||||
|
std::sort(values.begin(), values.end(), [](const auto & lhs, const auto & rhs) { return lhs.first < rhs.first; });
|
||||||
|
|
||||||
|
nix_value * a = values[0].second;
|
||||||
|
ASSERT_EQ("a", values[0].first);
|
||||||
|
ASSERT_EQ(nix_get_int(ctx, a), 2);
|
||||||
|
assert_ctx_ok();
|
||||||
|
nix_value * b = values[1].second;
|
||||||
|
ASSERT_EQ("b", values[1].first);
|
||||||
|
ASSERT_EQ(nix_get_int(ctx, b), 3);
|
||||||
|
assert_ctx_ok();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace nixC
|
} // namespace nixC
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ Bindings * EvalState::allocBindings(size_t capacity)
|
||||||
{
|
{
|
||||||
if (capacity == 0)
|
if (capacity == 0)
|
||||||
return &Bindings::emptyBindings;
|
return &Bindings::emptyBindings;
|
||||||
if (capacity > std::numeric_limits<Bindings::size_t>::max())
|
if (capacity > std::numeric_limits<Bindings::size_type>::max())
|
||||||
throw Error("attribute set of size %d is too big", capacity);
|
throw Error("attribute set of size %d is too big", capacity);
|
||||||
nrAttrsets++;
|
nrAttrsets++;
|
||||||
nrAttrsInAttrsets += capacity;
|
nrAttrsInAttrsets += capacity;
|
||||||
|
|
@ -35,7 +35,7 @@ Value & BindingsBuilder::alloc(std::string_view name, PosIdx pos)
|
||||||
|
|
||||||
void Bindings::sort()
|
void Bindings::sort()
|
||||||
{
|
{
|
||||||
std::sort(attrs, attrs + size_);
|
std::sort(attrs, attrs + numAttrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
Value & Value::mkAttrs(BindingsBuilder & bindings)
|
Value & Value::mkAttrs(BindingsBuilder & bindings)
|
||||||
|
|
|
||||||
|
|
@ -1873,37 +1873,71 @@ void ExprOpUpdate::eval(EvalState & state, Env & env, Value & v)
|
||||||
|
|
||||||
state.nrOpUpdates++;
|
state.nrOpUpdates++;
|
||||||
|
|
||||||
if (v1.attrs()->size() == 0) {
|
const Bindings & bindings1 = *v1.attrs();
|
||||||
|
if (bindings1.empty()) {
|
||||||
v = v2;
|
v = v2;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (v2.attrs()->size() == 0) {
|
|
||||||
|
const Bindings & bindings2 = *v2.attrs();
|
||||||
|
if (bindings2.empty()) {
|
||||||
v = v1;
|
v = v1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto attrs = state.buildBindings(v1.attrs()->size() + v2.attrs()->size());
|
/* Simple heuristic for determining whether attrs2 should be "layered" on top of
|
||||||
|
attrs1 instead of copying to a new Bindings. */
|
||||||
|
bool shouldLayer = [&]() -> bool {
|
||||||
|
if (bindings1.isLayerListFull())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (bindings2.size() > state.settings.bindingsUpdateLayerRhsSizeThreshold)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (shouldLayer) {
|
||||||
|
auto attrs = state.buildBindings(bindings2.size());
|
||||||
|
attrs.layerOnTopOf(bindings1);
|
||||||
|
|
||||||
|
std::ranges::copy(bindings2, std::back_inserter(attrs));
|
||||||
|
v.mkAttrs(attrs.alreadySorted());
|
||||||
|
|
||||||
|
state.nrOpUpdateValuesCopied += bindings2.size();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto attrs = state.buildBindings(bindings1.size() + bindings2.size());
|
||||||
|
|
||||||
/* Merge the sets, preferring values from the second set. Make
|
/* Merge the sets, preferring values from the second set. Make
|
||||||
sure to keep the resulting vector in sorted order. */
|
sure to keep the resulting vector in sorted order. */
|
||||||
auto i = v1.attrs()->begin();
|
auto i = bindings1.begin();
|
||||||
auto j = v2.attrs()->begin();
|
auto j = bindings2.begin();
|
||||||
|
|
||||||
while (i != v1.attrs()->end() && j != v2.attrs()->end()) {
|
while (i != bindings1.end() && j != bindings2.end()) {
|
||||||
if (i->name == j->name) {
|
if (i->name == j->name) {
|
||||||
attrs.insert(*j);
|
attrs.insert(*j);
|
||||||
++i;
|
++i;
|
||||||
++j;
|
++j;
|
||||||
} else if (i->name < j->name)
|
} else if (i->name < j->name) {
|
||||||
attrs.insert(*i++);
|
attrs.insert(*i);
|
||||||
else
|
++i;
|
||||||
attrs.insert(*j++);
|
} else {
|
||||||
|
attrs.insert(*j);
|
||||||
|
++j;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (i != v1.attrs()->end())
|
while (i != bindings1.end()) {
|
||||||
attrs.insert(*i++);
|
attrs.insert(*i);
|
||||||
while (j != v2.attrs()->end())
|
++i;
|
||||||
attrs.insert(*j++);
|
}
|
||||||
|
|
||||||
|
while (j != bindings2.end()) {
|
||||||
|
attrs.insert(*j);
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
|
||||||
v.mkAttrs(attrs.alreadySorted());
|
v.mkAttrs(attrs.alreadySorted());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@
|
||||||
#include "nix/expr/nixexpr.hh"
|
#include "nix/expr/nixexpr.hh"
|
||||||
#include "nix/expr/symbol-table.hh"
|
#include "nix/expr/symbol-table.hh"
|
||||||
|
|
||||||
|
#include <boost/container/static_vector.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <concepts>
|
#include <ranges>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
|
||||||
|
|
@ -48,11 +51,18 @@ static_assert(
|
||||||
* by its size and its capacity, the capacity being the number of Attr
|
* by its size and its capacity, the capacity being the number of Attr
|
||||||
* elements allocated after this structure, while the size corresponds to
|
* elements allocated after this structure, while the size corresponds to
|
||||||
* the number of elements already inserted in this structure.
|
* the number of elements already inserted in this structure.
|
||||||
|
*
|
||||||
|
* Bindings can be efficiently `//`-composed into an intrusive linked list of "layers"
|
||||||
|
* that saves on copies and allocations. Each lookup (@see Bindings::get) traverses
|
||||||
|
* this linked list until a matching attribute is found (thus overlays earlier in
|
||||||
|
* the list take precedence). For iteration over the whole Bindings, an on-the-fly
|
||||||
|
* k-way merge is performed by Bindings::iterator class.
|
||||||
*/
|
*/
|
||||||
class Bindings
|
class Bindings
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
typedef uint32_t size_t;
|
using size_type = uint32_t;
|
||||||
|
|
||||||
PosIdx pos;
|
PosIdx pos;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -62,7 +72,32 @@ public:
|
||||||
static Bindings emptyBindings;
|
static Bindings emptyBindings;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
size_t size_ = 0;
|
/**
|
||||||
|
* Number of attributes in the attrs FAM (Flexible Array Member).
|
||||||
|
*/
|
||||||
|
size_type numAttrs = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of attributes with unique names in the layer chain.
|
||||||
|
*
|
||||||
|
* This is the *real* user-facing size of bindings, whereas @ref numAttrs is
|
||||||
|
* an implementation detail of the data structure.
|
||||||
|
*/
|
||||||
|
size_type numAttrsInChain = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Length of the layers list.
|
||||||
|
*/
|
||||||
|
uint32_t numLayers = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bindings that this attrset is "layered" on top of.
|
||||||
|
*/
|
||||||
|
const Bindings * baseLayer = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flexible array member of attributes.
|
||||||
|
*/
|
||||||
Attr attrs[0];
|
Attr attrs[0];
|
||||||
|
|
||||||
Bindings() = default;
|
Bindings() = default;
|
||||||
|
|
@ -71,15 +106,22 @@ private:
|
||||||
Bindings & operator=(const Bindings &) = delete;
|
Bindings & operator=(const Bindings &) = delete;
|
||||||
Bindings & operator=(Bindings &&) = delete;
|
Bindings & operator=(Bindings &&) = delete;
|
||||||
|
|
||||||
|
friend class BindingsBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum length of the Bindings layer chains.
|
||||||
|
*/
|
||||||
|
static constexpr unsigned maxLayers = 8;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
size_t size() const
|
size_type size() const
|
||||||
{
|
{
|
||||||
return size_;
|
return numAttrsInChain;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool empty() const
|
bool empty() const
|
||||||
{
|
{
|
||||||
return !size_;
|
return size() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
class iterator
|
class iterator
|
||||||
|
|
@ -94,77 +136,276 @@ public:
|
||||||
friend class Bindings;
|
friend class Bindings;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
pointer ptr = nullptr;
|
struct BindingsCursor
|
||||||
|
|
||||||
explicit iterator(pointer ptr)
|
|
||||||
: ptr(ptr)
|
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Attr that the cursor currently points to.
|
||||||
|
*/
|
||||||
|
pointer current;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One past the end pointer to the contiguous buffer of Attrs.
|
||||||
|
*/
|
||||||
|
pointer end;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Priority of the value. Lesser values have more priority (i.e. they override
|
||||||
|
* attributes that appear later in the linked list of Bindings).
|
||||||
|
*/
|
||||||
|
uint32_t priority;
|
||||||
|
|
||||||
|
pointer operator->() const noexcept
|
||||||
|
{
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
reference get() const noexcept
|
||||||
|
{
|
||||||
|
return *current;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool empty() const noexcept
|
||||||
|
{
|
||||||
|
return current == end;
|
||||||
|
}
|
||||||
|
|
||||||
|
void increment() noexcept
|
||||||
|
{
|
||||||
|
++current;
|
||||||
|
}
|
||||||
|
|
||||||
|
void consume(Symbol name) noexcept
|
||||||
|
{
|
||||||
|
while (!empty() && current->name <= name)
|
||||||
|
++current;
|
||||||
|
}
|
||||||
|
|
||||||
|
GENERATE_CMP(BindingsCursor, me->current->name, me->priority)
|
||||||
|
};
|
||||||
|
|
||||||
|
using QueueStorageType = boost::container::static_vector<BindingsCursor, maxLayers>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comparator implementing the override priority / name ordering
|
||||||
|
* for BindingsCursor.
|
||||||
|
*/
|
||||||
|
static constexpr auto comp = std::greater<BindingsCursor>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A priority queue used to implement an on-the-fly k-way merge.
|
||||||
|
*/
|
||||||
|
QueueStorageType cursorHeap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attribute the iterator currently points to.
|
||||||
|
*/
|
||||||
|
pointer current = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether iterating over a single attribute and not a merge chain.
|
||||||
|
*/
|
||||||
|
bool doMerge = true;
|
||||||
|
|
||||||
|
void push(BindingsCursor cursor) noexcept
|
||||||
|
{
|
||||||
|
cursorHeap.push_back(cursor);
|
||||||
|
std::ranges::make_heap(cursorHeap, comp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] BindingsCursor pop() noexcept
|
||||||
|
{
|
||||||
|
std::ranges::pop_heap(cursorHeap, comp);
|
||||||
|
auto cursor = cursorHeap.back();
|
||||||
|
cursorHeap.pop_back();
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
iterator & finished() noexcept
|
||||||
|
{
|
||||||
|
current = nullptr;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void next(BindingsCursor cursor) noexcept
|
||||||
|
{
|
||||||
|
current = &cursor.get();
|
||||||
|
cursor.increment();
|
||||||
|
|
||||||
|
if (!cursor.empty())
|
||||||
|
push(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BindingsCursor> consumeAllUntilCurrentName() noexcept
|
||||||
|
{
|
||||||
|
auto cursor = pop();
|
||||||
|
Symbol lastHandledName = current->name;
|
||||||
|
|
||||||
|
while (cursor->name <= lastHandledName) {
|
||||||
|
cursor.consume(lastHandledName);
|
||||||
|
if (!cursor.empty())
|
||||||
|
push(cursor);
|
||||||
|
|
||||||
|
if (cursorHeap.empty())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
cursor = pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit iterator(const Bindings & attrs) noexcept
|
||||||
|
: doMerge(attrs.baseLayer)
|
||||||
|
{
|
||||||
|
auto pushBindings = [this, priority = unsigned{0}](const Bindings & layer) mutable {
|
||||||
|
auto first = layer.attrs;
|
||||||
|
push(
|
||||||
|
BindingsCursor{
|
||||||
|
.current = first,
|
||||||
|
.end = first + layer.numAttrs,
|
||||||
|
.priority = priority++,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!doMerge) {
|
||||||
|
if (attrs.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
current = attrs.attrs;
|
||||||
|
pushBindings(attrs);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Bindings * layer = &attrs;
|
||||||
|
while (layer) {
|
||||||
|
if (layer->numAttrs != 0)
|
||||||
|
pushBindings(*layer);
|
||||||
|
layer = layer->baseLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorHeap.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
next(pop());
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
iterator() = default;
|
iterator() = default;
|
||||||
|
|
||||||
reference operator*() const
|
reference operator*() const noexcept
|
||||||
{
|
{
|
||||||
return *ptr;
|
return *current;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value_type * operator->() const
|
pointer operator->() const noexcept
|
||||||
{
|
{
|
||||||
return ptr;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
iterator & operator++()
|
iterator & operator++() noexcept
|
||||||
{
|
{
|
||||||
++ptr;
|
if (!doMerge) {
|
||||||
|
++current;
|
||||||
|
if (current == cursorHeap.front().end)
|
||||||
|
return finished();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorHeap.empty())
|
||||||
|
return finished();
|
||||||
|
|
||||||
|
auto cursor = consumeAllUntilCurrentName();
|
||||||
|
if (!cursor)
|
||||||
|
return finished();
|
||||||
|
|
||||||
|
next(*cursor);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
iterator operator++(int)
|
iterator operator++(int) noexcept
|
||||||
{
|
{
|
||||||
pointer tmp = ptr;
|
iterator tmp = *this;
|
||||||
++*this;
|
++*this;
|
||||||
return iterator(tmp);
|
return tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator==(const iterator & rhs) const = default;
|
bool operator==(const iterator & rhs) const noexcept
|
||||||
|
{
|
||||||
|
return current == rhs.current;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
using const_iterator = iterator;
|
using const_iterator = iterator;
|
||||||
|
|
||||||
void push_back(const Attr & attr)
|
void push_back(const Attr & attr)
|
||||||
{
|
{
|
||||||
attrs[size_++] = attr;
|
attrs[numAttrs++] = attr;
|
||||||
|
numAttrsInChain = numAttrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Attr * get(Symbol name) const
|
/**
|
||||||
|
* Get attribute by name or nullptr if no such attribute exists.
|
||||||
|
*/
|
||||||
|
const Attr * get(Symbol name) const noexcept
|
||||||
{
|
{
|
||||||
Attr key(name, 0);
|
auto getInChunk = [key = Attr{name, nullptr}](const Bindings & chunk) -> const Attr * {
|
||||||
auto first = attrs;
|
auto first = chunk.attrs;
|
||||||
auto last = attrs + size_;
|
auto last = first + chunk.numAttrs;
|
||||||
const Attr * i = std::lower_bound(first, last, key);
|
const Attr * i = std::lower_bound(first, last, key);
|
||||||
if (i != last && i->name == name)
|
if (i != last && i->name == key.name)
|
||||||
return i;
|
return i;
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Bindings * currentChunk = this;
|
||||||
|
while (currentChunk) {
|
||||||
|
const Attr * maybeAttr = getInChunk(*currentChunk);
|
||||||
|
if (maybeAttr)
|
||||||
|
return maybeAttr;
|
||||||
|
currentChunk = currentChunk->baseLayer;
|
||||||
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the layer chain is full.
|
||||||
|
*/
|
||||||
|
bool isLayerListFull() const noexcept
|
||||||
|
{
|
||||||
|
return numLayers == Bindings::maxLayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if the length of the linked list of layers is greater than 1.
|
||||||
|
*/
|
||||||
|
bool isLayered() const noexcept
|
||||||
|
{
|
||||||
|
return numLayers > 1;
|
||||||
|
}
|
||||||
|
|
||||||
const_iterator begin() const
|
const_iterator begin() const
|
||||||
{
|
{
|
||||||
return const_iterator(attrs);
|
return const_iterator(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
const_iterator end() const
|
const_iterator end() const
|
||||||
{
|
{
|
||||||
return const_iterator(attrs + size_);
|
return const_iterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
Attr & operator[](size_t pos)
|
Attr & operator[](size_type pos)
|
||||||
{
|
{
|
||||||
|
if (isLayered()) [[unlikely]]
|
||||||
|
unreachable();
|
||||||
return attrs[pos];
|
return attrs[pos];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Attr & operator[](size_t pos) const
|
const Attr & operator[](size_type pos) const
|
||||||
{
|
{
|
||||||
|
if (isLayered()) [[unlikely]]
|
||||||
|
unreachable();
|
||||||
return attrs[pos];
|
return attrs[pos];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,10 +417,9 @@ public:
|
||||||
std::vector<const Attr *> lexicographicOrder(const SymbolTable & symbols) const
|
std::vector<const Attr *> lexicographicOrder(const SymbolTable & symbols) const
|
||||||
{
|
{
|
||||||
std::vector<const Attr *> res;
|
std::vector<const Attr *> res;
|
||||||
res.reserve(size_);
|
res.reserve(size());
|
||||||
for (size_t n = 0; n < size_; n++)
|
std::ranges::transform(*this, std::back_inserter(res), [](const Attr & a) { return &a; });
|
||||||
res.emplace_back(&attrs[n]);
|
std::ranges::sort(res, [&](const Attr * a, const Attr * b) {
|
||||||
std::sort(res.begin(), res.end(), [&](const Attr * a, const Attr * b) {
|
|
||||||
std::string_view sa = symbols[a->name], sb = symbols[b->name];
|
std::string_view sa = symbols[a->name], sb = symbols[b->name];
|
||||||
return sa < sb;
|
return sa < sb;
|
||||||
});
|
});
|
||||||
|
|
@ -202,11 +442,11 @@ class BindingsBuilder final
|
||||||
public:
|
public:
|
||||||
// needed by std::back_inserter
|
// needed by std::back_inserter
|
||||||
using value_type = Attr;
|
using value_type = Attr;
|
||||||
using size_type = Bindings::size_t;
|
using size_type = Bindings::size_type;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Bindings * bindings;
|
Bindings * bindings;
|
||||||
Bindings::size_t capacity_;
|
Bindings::size_type capacity_;
|
||||||
|
|
||||||
friend class EvalState;
|
friend class EvalState;
|
||||||
|
|
||||||
|
|
@ -217,6 +457,19 @@ private:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasBaseLayer() const noexcept
|
||||||
|
{
|
||||||
|
return bindings->baseLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void finishSizeIfNecessary()
|
||||||
|
{
|
||||||
|
if (hasBaseLayer())
|
||||||
|
/* NOTE: Do not use std::ranges::distance, since Bindings is a sized
|
||||||
|
range, but we are calculating this size here. */
|
||||||
|
bindings->numAttrsInChain = std::distance(bindings->begin(), bindings->end());
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
std::reference_wrapper<EvalState> state;
|
std::reference_wrapper<EvalState> state;
|
||||||
|
|
||||||
|
|
@ -232,10 +485,26 @@ public:
|
||||||
|
|
||||||
void push_back(const Attr & attr)
|
void push_back(const Attr & attr)
|
||||||
{
|
{
|
||||||
assert(bindings->size() < capacity_);
|
assert(bindings->numAttrs < capacity_);
|
||||||
bindings->push_back(attr);
|
bindings->push_back(attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Layer" the newly constructured Bindings on top of another attribute set.
|
||||||
|
*
|
||||||
|
* This effectively performs an attribute set merge, while giving preference
|
||||||
|
* to attributes from the newly constructed Bindings in case of duplicate attribute
|
||||||
|
* names.
|
||||||
|
*
|
||||||
|
* This operation amortizes the need to copy over all attributes and allows
|
||||||
|
* for efficient implementation of attribute set merges (ExprOpUpdate::eval).
|
||||||
|
*/
|
||||||
|
void layerOnTopOf(const Bindings & base) noexcept
|
||||||
|
{
|
||||||
|
bindings->baseLayer = &base;
|
||||||
|
bindings->numLayers = base.numLayers + 1;
|
||||||
|
}
|
||||||
|
|
||||||
Value & alloc(Symbol name, PosIdx pos = noPos);
|
Value & alloc(Symbol name, PosIdx pos = noPos);
|
||||||
|
|
||||||
Value & alloc(std::string_view name, PosIdx pos = noPos);
|
Value & alloc(std::string_view name, PosIdx pos = noPos);
|
||||||
|
|
@ -243,11 +512,13 @@ public:
|
||||||
Bindings * finish()
|
Bindings * finish()
|
||||||
{
|
{
|
||||||
bindings->sort();
|
bindings->sort();
|
||||||
|
finishSizeIfNecessary();
|
||||||
return bindings;
|
return bindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bindings * alreadySorted()
|
Bindings * alreadySorted()
|
||||||
{
|
{
|
||||||
|
finishSizeIfNecessary();
|
||||||
return bindings;
|
return bindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -342,6 +342,25 @@ struct EvalSettings : Config
|
||||||
This is useful for improving code readability and making path literals
|
This is useful for improving code readability and making path literals
|
||||||
more explicit.
|
more explicit.
|
||||||
)"};
|
)"};
|
||||||
|
|
||||||
|
Setting<unsigned> bindingsUpdateLayerRhsSizeThreshold{
|
||||||
|
this,
|
||||||
|
sizeof(void *) == 4 ? 8192 : 16,
|
||||||
|
"eval-attrset-update-layer-rhs-threshold",
|
||||||
|
R"(
|
||||||
|
Tunes the maximum size of an attribute set that, when used
|
||||||
|
as a right operand in an [attribute set update expression](@docroot@/language/operators.md#update),
|
||||||
|
uses a more space-efficient linked-list representation of attribute sets.
|
||||||
|
|
||||||
|
Setting this to larger values generally leads to less memory allocations,
|
||||||
|
but may lead to worse evaluation performance.
|
||||||
|
|
||||||
|
A value of `0` disables this optimization completely.
|
||||||
|
|
||||||
|
This is an advanced performance tuning option and typically should not be changed.
|
||||||
|
The default value is chosen to balance performance and memory usage. On 32 bit systems
|
||||||
|
where memory is scarce, the default is a large value to reduce the amount of allocations.
|
||||||
|
)"};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue