Add low-overhead InPlaceAppender (#8789)

- Internal for now.
- Previously known as `FixedAppender` (during development).

Co-authored-by: Elias Batek <0xEAB@users.noreply.github.com>
This commit is contained in:
John Colvin 2025-01-19 00:03:43 +00:00 committed by GitHub
parent 1b242048c9
commit bea3184479
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -3571,18 +3571,11 @@ See_Also: $(LREF appender)
struct Appender(A) struct Appender(A)
if (isDynamicArray!A) if (isDynamicArray!A)
{ {
import core.memory : GC; import std.format.spec : FormatSpec;
private alias T = ElementEncodingType!A; private alias T = ElementEncodingType!A;
private struct Data InPlaceAppender!A* impl;
{
size_t capacity;
Unqual!T[] arr;
bool tryExtendBlock = false;
}
private Data* _data;
/** /**
* Constructs an `Appender` with a given array. Note that this does not copy the * Constructs an `Appender` with a given array. Note that this does not copy the
@ -3590,27 +3583,17 @@ if (isDynamicArray!A)
* it will be used by the appender. After initializing an appender on an array, * it will be used by the appender. After initializing an appender on an array,
* appending to the original array will reallocate. * appending to the original array will reallocate.
*/ */
this(A arr) @trusted this(A arr) @safe
{ {
// initialize to a given array. impl = new InPlaceAppender!A(arr);
_data = new Data; }
_data.arr = cast(Unqual!T[]) arr; //trusted
private void ensureInit() @safe
if (__ctfe) {
return; if (impl is null)
{
// We want to use up as much of the block the array is in as possible. impl = new InPlaceAppender!A;
// if we consume all the block that we can, then array appending is
// safe WRT built-in append, and we can use the entire block.
// We only do this for mutable types that can be extended.
static if (isMutable!T && is(typeof(arr.length = size_t.max)))
{
immutable cap = arr.capacity; //trusted
// Replace with "GC.setAttr( Not Appendable )" once pure (and fixed)
if (cap > arr.length)
arr.length = cap;
} }
_data.capacity = arr.length;
} }
/** /**
@ -3623,14 +3606,10 @@ if (isDynamicArray!A)
*/ */
void reserve(size_t newCapacity) void reserve(size_t newCapacity)
{ {
if (_data) if (newCapacity != 0)
{ {
if (newCapacity > _data.capacity) ensureInit();
ensureAddable(newCapacity - _data.arr.length); impl.reserve(newCapacity);
}
else
{
ensureAddable(newCapacity);
} }
} }
@ -3641,11 +3620,11 @@ if (isDynamicArray!A)
*/ */
@property size_t capacity() const @property size_t capacity() const
{ {
return _data ? _data.capacity : 0; return impl ? impl.capacity : 0;
} }
/// Returns: The number of elements appended. /// Returns: The number of elements appended.
@property size_t length() const => _data ? _data.arr.length : 0; @property size_t length() const => (impl is null) ? 0 : impl.length;
/** /**
* Use opSlice() from now on. * Use opSlice() from now on.
@ -3653,29 +3632,219 @@ if (isDynamicArray!A)
*/ */
@property inout(T)[] data() inout @property inout(T)[] data() inout
{ {
return this[]; return opSlice();
} }
/** /**
* Returns: The managed array. * Returns: The managed array.
*/ */
@property inout(T)[] opSlice() inout @trusted @property inout(T)[] opSlice() inout @safe
{
return impl ? impl.opSlice() : null;
}
/**
* Appends `item` to the managed array. Performs encoding for
* `char` types if `A` is a differently typed `char` array.
*
* Params:
* item = the single item to append
*/
void put(U)(U item)
if (InPlaceAppender!A.canPutItem!U)
{
ensureInit();
impl.put(item);
}
// Const fixing hack.
void put(Range)(Range items)
if (InPlaceAppender!A.canPutConstRange!Range)
{
if (!items.empty)
{
ensureInit();
impl.put(items);
}
}
/**
* Appends an entire range to the managed array. Performs encoding for
* `char` elements if `A` is a differently typed `char` array.
*
* Params:
* items = the range of items to append
*/
void put(Range)(Range items)
if (InPlaceAppender!A.canPutRange!Range)
{
if (!items.empty)
{
ensureInit();
impl.put(items);
}
}
/**
* Appends to the managed array.
*
* See_Also: $(LREF Appender.put)
*/
alias opOpAssign(string op : "~") = put;
// only allow overwriting data on non-immutable and non-const data
static if (isMutable!T)
{
/**
* Clears the managed array. This allows the elements of the array to be reused
* for appending.
*
* Note: clear is disabled for immutable or const element types, due to the
* possibility that `Appender` might overwrite immutable data.
*/
void clear() @safe pure nothrow
{
if (impl)
{
impl.clear();
}
}
/**
* Shrinks the managed array to the given length.
*
* Throws: `Exception` if newlength is greater than the current array length.
* Note: shrinkTo is disabled for immutable or const element types.
*/
void shrinkTo(size_t newlength) @safe pure
{
import std.exception : enforce;
if (impl)
{
impl.shrinkTo(newlength);
}
else
{
enforce(newlength == 0, "Attempting to shrink empty Appender with non-zero newlength");
}
}
}
/**
* Gives a string in the form of `Appender!(A)(data)`.
*
* Params:
* w = A `char` accepting
* $(REF_ALTTEXT output range, isOutputRange, std, range, primitives).
* fmt = A $(REF FormatSpec, std, format) which controls how the array
* is formatted.
* Returns:
* A `string` if `writer` is not set; `void` otherwise.
*/
string toString()() const
{
return InPlaceAppender!A.toStringImpl(Unqual!(typeof(this)).stringof, impl ? impl.data : null);
}
/// ditto
template toString(Writer)
if (isOutputRange!(Writer, char))
{
void toString(scope ref Writer w, scope const ref FormatSpec!char fmt) const
{
InPlaceAppender!A.toStringImpl(Unqual!(typeof(this)).stringof, impl ? impl.data : null, w, fmt);
}
}
}
///
@safe pure nothrow unittest
{
auto app = appender!string();
string b = "abcdefg";
foreach (char c; b)
app.put(c);
assert(app[] == "abcdefg");
int[] a = [ 1, 2 ];
auto app2 = appender(a);
app2.put(3);
app2.put([ 4, 5, 6 ]);
assert(app2[] == [ 1, 2, 3, 4, 5, 6 ]);
}
package(std) struct InPlaceAppender(A)
if (isDynamicArray!A)
{
import core.memory : GC;
import std.format.spec : FormatSpec;
private alias T = ElementEncodingType!A;
private
{
size_t _capacity;
Unqual!T[] arr;
bool tryExtendBlock = false;
}
@disable this(ref InPlaceAppender);
this(A arrIn) @trusted
{
arr = cast(Unqual!T[]) arrIn; //trusted
if (__ctfe)
return;
// We want to use up as much of the block the array is in as possible.
// if we consume all the block that we can, then array appending is
// safe WRT built-in append, and we can use the entire block.
// We only do this for mutable types that can be extended.
static if (isMutable!T && is(typeof(arrIn.length = size_t.max)))
{
immutable cap = arrIn.capacity; //trusted
// Replace with "GC.setAttr( Not Appendable )" once pure (and fixed)
if (cap > arrIn.length)
arrIn.length = cap;
}
_capacity = arrIn.length;
}
void reserve(size_t newCapacity)
{
if (newCapacity > _capacity)
ensureAddable(newCapacity - arr.length);
}
@property size_t capacity() const
{
return _capacity;
}
@property size_t length() const => arr.length;
@property inout(T)[] data() inout
{
return this[];
}
inout(T)[] opSlice() inout @trusted
{ {
/* @trusted operation: /* @trusted operation:
* casting Unqual!T[] to inout(T)[] * casting Unqual!T[] to inout(T)[]
*/ */
return cast(typeof(return))(_data ? _data.arr : null); return cast(typeof(return)) arr;
} }
// ensure we can add nelems elements, resizing as necessary // ensure we can add nelems elements, resizing as necessary
private void ensureAddable(size_t nelems) private void ensureAddable(size_t nelems)
{ {
if (!_data) immutable len = arr.length;
_data = new Data;
immutable len = _data.arr.length;
immutable reqlen = len + nelems; immutable reqlen = len + nelems;
if (_data.capacity >= reqlen) if (_capacity >= reqlen)
return; return;
// need to increase capacity // need to increase capacity
@ -3683,17 +3852,17 @@ if (isDynamicArray!A)
{ {
static if (__traits(compiles, new Unqual!T[1])) static if (__traits(compiles, new Unqual!T[1]))
{ {
_data.arr.length = reqlen; arr.length = reqlen;
} }
else else
{ {
// avoid restriction of @disable this() // avoid restriction of @disable this()
_data.arr = _data.arr[0 .. _data.capacity]; arr = arr[0 .. _capacity];
foreach (i; _data.capacity .. reqlen) foreach (i; _capacity .. reqlen)
_data.arr ~= Unqual!T.init; arr ~= Unqual!T.init;
} }
_data.arr = _data.arr[0 .. len]; arr = arr[0 .. len];
_data.capacity = reqlen; _capacity = reqlen;
} }
else else
{ {
@ -3701,11 +3870,11 @@ if (isDynamicArray!A)
// Time to reallocate. // Time to reallocate.
// We need to almost duplicate what's in druntime, except we // We need to almost duplicate what's in druntime, except we
// have better access to the capacity field. // have better access to the capacity field.
auto newlen = appenderNewCapacity!(T.sizeof)(_data.capacity, reqlen); auto newlen = appenderNewCapacity!(T.sizeof)(_capacity, reqlen);
// first, try extending the current block // first, try extending the current block
if (_data.tryExtendBlock) if (tryExtendBlock)
{ {
immutable u = (() @trusted => GC.extend(_data.arr.ptr, nelems * T.sizeof, (newlen - len) * T.sizeof))(); immutable u = (() @trusted => GC.extend(arr.ptr, nelems * T.sizeof, (newlen - len) * T.sizeof))();
if (u) if (u)
{ {
// extend worked, update the capacity // extend worked, update the capacity
@ -3714,11 +3883,10 @@ if (isDynamicArray!A)
// at large unused blocks. // at large unused blocks.
static if (hasIndirections!T) static if (hasIndirections!T)
{ {
immutable addedSize = u - (_data.capacity * T.sizeof); immutable addedSize = u - (_capacity * T.sizeof);
() @trusted { memset(_data.arr.ptr + _data.capacity, 0, addedSize); }(); () @trusted { memset(arr.ptr + _capacity, 0, addedSize); }();
} }
_capacity = u / T.sizeof;
_data.capacity = u / T.sizeof;
return; return;
} }
} }
@ -3732,11 +3900,11 @@ if (isDynamicArray!A)
~ "available pointer range"); ~ "available pointer range");
auto bi = (() @trusted => GC.qalloc(nbytes, blockAttribute!T))(); auto bi = (() @trusted => GC.qalloc(nbytes, blockAttribute!T))();
_data.capacity = bi.size / T.sizeof; _capacity = bi.size / T.sizeof;
import core.stdc.string : memcpy;
if (len) if (len)
() @trusted { memcpy(bi.base, _data.arr.ptr, len * T.sizeof); }(); () @trusted { memcpy(bi.base, arr.ptr, len * T.sizeof); }();
arr = (() @trusted => (cast(Unqual!T*) bi.base)[0 .. len])();
_data.arr = (() @trusted => (cast(Unqual!T*) bi.base)[0 .. len])();
// we requested new bytes that are not in the existing // we requested new bytes that are not in the existing
// data. If T has pointers, then this new data could point at stale // data. If T has pointers, then this new data could point at stale
@ -3747,7 +3915,7 @@ if (isDynamicArray!A)
memset(bi.base + (len * T.sizeof), 0, (newlen - len) * T.sizeof); memset(bi.base + (len * T.sizeof), 0, (newlen - len) * T.sizeof);
}(); }();
_data.tryExtendBlock = true; tryExtendBlock = true;
// leave the old data, for safety reasons // leave the old data, for safety reasons
} }
} }
@ -3763,13 +3931,13 @@ if (isDynamicArray!A)
enum bool canPutConstRange = enum bool canPutConstRange =
isInputRange!(Unqual!Range) && isInputRange!(Unqual!Range) &&
!isInputRange!Range && !isInputRange!Range &&
is(typeof(Appender.init.put(Range.init.front))); is(typeof(InPlaceAppender.init.put(Range.init.front)));
} }
private template canPutRange(Range) private template canPutRange(Range)
{ {
enum bool canPutRange = enum bool canPutRange =
isInputRange!Range && isInputRange!Range &&
is(typeof(Appender.init.put(Range.init.front))); is(typeof(InPlaceAppender.init.put(Range.init.front)));
} }
/** /**
@ -3798,13 +3966,13 @@ if (isDynamicArray!A)
import core.lifetime : emplace; import core.lifetime : emplace;
ensureAddable(1); ensureAddable(1);
immutable len = _data.arr.length; immutable len = arr.length;
auto bigData = (() @trusted => _data.arr.ptr[0 .. len + 1])(); auto bigData = (() @trusted => arr.ptr[0 .. len + 1])();
auto itemUnqual = (() @trusted => & cast() item)(); auto itemUnqual = (() @trusted => & cast() item)();
emplace(&bigData[len], *itemUnqual); emplace(&bigData[len], *itemUnqual);
//We do this at the end, in case of exceptions //We do this at the end, in case of exceptions
_data.arr = bigData; arr = bigData;
} }
} }
@ -3848,16 +4016,16 @@ if (isDynamicArray!A)
auto bigDataFun(size_t extra) auto bigDataFun(size_t extra)
{ {
ensureAddable(extra); ensureAddable(extra);
return (() @trusted => _data.arr.ptr[0 .. _data.arr.length + extra])(); return (() @trusted => arr.ptr[0 .. arr.length + extra])();
} }
auto bigData = bigDataFun(items.length); auto bigData = bigDataFun(items.length);
immutable len = _data.arr.length; immutable len = arr.length;
immutable newlen = bigData.length; immutable newlen = bigData.length;
alias UT = Unqual!T; alias UT = Unqual!T;
static if (is(typeof(_data.arr[] = items[])) && static if (is(typeof(arr[] = items[])) &&
!hasElaborateAssign!UT && isAssignable!(UT, ElementEncodingType!Range)) !hasElaborateAssign!UT && isAssignable!(UT, ElementEncodingType!Range))
{ {
bigData[len .. newlen] = items[]; bigData[len .. newlen] = items[];
@ -3873,7 +4041,7 @@ if (isDynamicArray!A)
} }
//We do this at the end, in case of exceptions //We do this at the end, in case of exceptions
_data.arr = bigData; arr = bigData;
} }
else static if (isSomeChar!T && isSomeChar!(ElementType!Range) && else static if (isSomeChar!T && isSomeChar!(ElementType!Range) &&
!is(immutable T == immutable ElementType!Range)) !is(immutable T == immutable ElementType!Range))
@ -3916,10 +4084,7 @@ if (isDynamicArray!A)
*/ */
void clear() @trusted pure nothrow void clear() @trusted pure nothrow
{ {
if (_data) arr = arr.ptr[0 .. 0];
{
_data.arr = _data.arr.ptr[0 .. 0];
}
} }
/** /**
@ -3931,13 +4096,8 @@ if (isDynamicArray!A)
void shrinkTo(size_t newlength) @trusted pure void shrinkTo(size_t newlength) @trusted pure
{ {
import std.exception : enforce; import std.exception : enforce;
if (_data) enforce(newlength <= arr.length, "Attempting to shrink Appender with newlength > length");
{ arr = arr.ptr[0 .. newlength];
enforce(newlength <= _data.arr.length, "Attempting to shrink Appender with newlength > length");
_data.arr = _data.arr.ptr[0 .. newlength];
}
else
enforce(newlength == 0, "Attempting to shrink empty Appender with non-zero newlength");
} }
} }
@ -3952,13 +4112,18 @@ if (isDynamicArray!A)
* Returns: * Returns:
* A `string` if `writer` is not set; `void` otherwise. * A `string` if `writer` is not set; `void` otherwise.
*/ */
string toString()() const auto toString() const
{
return toStringImpl(Unqual!(typeof(this)).stringof, data);
}
static auto toStringImpl(string typeName, const T[] arr)
{ {
import std.format.spec : singleSpec; import std.format.spec : singleSpec;
auto app = appender!string(); InPlaceAppender!string app;
auto spec = singleSpec("%s"); auto spec = singleSpec("%s");
immutable len = _data ? _data.arr.length : 0; immutable len = arr.length;
// different reserve lengths because each element in a // different reserve lengths because each element in a
// non-string-like array uses two extra characters for `, `. // non-string-like array uses two extra characters for `, `.
static if (isSomeString!A) static if (isSomeString!A)
@ -3971,26 +4136,26 @@ if (isDynamicArray!A)
// length, as it assumes each element is only one char // length, as it assumes each element is only one char
app.reserve((len * 3) + 25); app.reserve((len * 3) + 25);
} }
toString(app, spec); toStringImpl(typeName, arr, app, spec);
return app.data; return app.data;
} }
import std.format.spec : FormatSpec; void toString(Writer)(scope ref Writer w, scope const ref FormatSpec!char fmt) const
/// ditto
template toString(Writer)
if (isOutputRange!(Writer, char)) if (isOutputRange!(Writer, char))
{ {
void toString(ref Writer w, scope const ref FormatSpec!char fmt) const toStringImpl(Unqual!(typeof(this)).stringof, data, w, fmt);
}
static void toStringImpl(Writer)(string typeName, const T[] data, scope ref Writer w,
scope const ref FormatSpec!char fmt)
{ {
import std.format.write : formatValue; import std.format.write : formatValue;
import std.range.primitives : put; import std.range.primitives : put;
put(w, Unqual!(typeof(this)).stringof); put(w, typeName);
put(w, '('); put(w, '(');
formatValue(w, data, fmt); formatValue(w, data, fmt);
put(w, ')'); put(w, ')');
} }
}
} }
/// ///
@ -4032,6 +4197,16 @@ if (isDynamicArray!A)
assert(app3[] == "Appender!(int[])(0001, 0002, 0003)"); assert(app3[] == "Appender!(int[])(0001, 0002, 0003)");
} }
@safe pure unittest
{
auto app = appender!(char[])();
app ~= "hello";
app.clear;
// not a promise, just nothing else exercises capacity
// and this is the expected sort of behaviour
assert(app.capacity >= 5);
}
// https://issues.dlang.org/show_bug.cgi?id=17251 // https://issues.dlang.org/show_bug.cgi?id=17251
@safe pure nothrow unittest @safe pure nothrow unittest
{ {
@ -4294,6 +4469,24 @@ unittest
assert(app2.capacity >= 5); assert(app2.capacity >= 5);
} }
/++
Convenience function that returns a $(LREF InPlaceAppender) instance,
optionally initialized with `array`.
+/
package(std) InPlaceAppender!A inPlaceAppender(A)()
if (isDynamicArray!A)
{
return InPlaceAppender!A(null);
}
/// ditto
package(std) InPlaceAppender!(E[]) inPlaceAppender(A : E[], E)(auto ref A array)
{
static assert(!isStaticArray!A || __traits(isRef, array),
"Cannot create InPlaceAppender from an rvalue static array");
return InPlaceAppender!(E[])(array);
}
/++ /++
Convenience function that returns an $(LREF Appender) instance, Convenience function that returns an $(LREF Appender) instance,
optionally initialized with `array`. optionally initialized with `array`.