Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allocAppendable() and getCapacity(). #284

Merged
merged 35 commits into from
Aug 24, 2023

Conversation

dsm9000
Copy link
Contributor

@dsm9000 dsm9000 commented Aug 15, 2023

  • Appendability metadata, currently for large-sized allocs only. All large allocs are now considered appendable.
  • allocAppendable(): requests an appendable block of given size and saves the used capacity in the extent metadata.
  • getCapacity(): computes the capacity of a memory segment per the rules of https://dlang.org/spec/arrays.html#capacity-reserve .
  • realloc(): modified to behave correctly on appendable allocs.
  • Tests.

Comment on lines 110 to 111
void* finalizer; // Reserved, not used currently
ubyte[47] _unused;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not useful.

@dsm9000 dsm9000 marked this pull request as draft August 15, 2023 13:21
Copy link
Contributor

@deadalnix deadalnix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like there is a lot of extraneous content in this that is not justified by how things are used.

There is a serious lack of tests. This obviously would inform on the previous point while also ensuring the details of what remains are right.

It would also be great if we could avoid opening numerous PR without addressing what can be addressed in the other ones.

// Metadata for non-slab (large) size classes
struct large {
ulong allocSize; // Actual alloc size, stored when required
bool canAppend; // Is the alloc appendable?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not seems to be useful either, because i can't think of a case where allocSize is going to be zero.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're saying that allocSize must always store the alloc size, then canAppend is required to represent appendability. (Even before we have finalizer and it will be necessary to store allocSize when finalization but not appendability is in use for a given alloc.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just store zero.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

Bitmap!512 _slabData = void;
union _meta {
// Slab occupancy (constrained by freeSlots, so usable for all classes)
Bitmap!512 slabOccupy;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change a perfectly good name?

Bitmap!512 slabOccupy;

// Metadata for non-slab (large) size classes
struct large {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Types are Capitalized.

@@ -99,7 +99,20 @@ private:
Links _links;

import d.gc.bitmap;
Bitmap!512 _slabData = void;
union _meta {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not follow the naming convention.

Comment on lines 142 to 145
void clearLargeMeta() {
meta.large.allocSize = 0;
meta.large.canAppend = false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After cleaning up the large data structure, this will end up being only one assignement, and it has one call site, so it can go.

auto ptr = arena.allocLarge(emap, aSize, false);
auto pd = getPageDescriptor(ptr);
pd.extent.allocSize = 0;
pd.extent.appendable = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And because it's not 0, we don't need this.


@property
ref bool appendable() {
assert(!isSlab(), "appendable accessed on slab!");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are enough !isSlab all over to call for an isLarge predicate. Also, there is slab feature section int here, so why isn't there a large feature section now that we are adding features for large extents?

Comment on lines 67 to 69
// Clear metadata:
if (!pd.isSlab())
pd.extent.clearLargeMeta();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me why this matters...

Comment on lines 175 to 178
// Transfer metadata (currently only support large allocs) :
auto pdNew = getPageDescriptor(newPtr);
if (!pd.isSlab() && !pdNew.isSlab())
pd.extent.copyLargeMeta(pdNew.extent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be wrong, as the copied capacity may not even fit in the new allocation's size!

Comment on lines 74 to 133
// Determine whether given alloc is appendable
bool is_appendable(void* ptr) {
if (ptr is null)
return false;

auto pd = getPageDescriptor(ptr);
// Not supports slab allocs yet
if (pd.isSlab())
return false;

return pd.extent.appendable;
}

// Get the current fill of an appendable alloc.
// If the alloc is not appendable, returns 0.
ulong get_appendable_fill(void* ptr) {
if (ptr is null)
return 0;

auto pd = getPageDescriptor(ptr);
// Not supports slab allocs yet
if (pd.isSlab() || !pd.extent.appendable)
return 0;

return pd.extent.allocSize;
}

// Get the current free space of an appendable alloc.
// If the alloc is not appendable, returns 0.
ulong get_appendable_free_space(void* ptr) {
if (ptr is null)
return 0;

auto pd = getPageDescriptor(ptr);
// Not supports slab allocs yet
if (pd.isSlab() || !pd.extent.appendable)
return 0;

return cast(ulong) pd.extent.size - pd.extent.allocSize;
}

// Change the current fill of an appendable alloc.
// If the alloc is not appendable, or the requested
// fill exceeds the available space, returns false.
bool set_appendable_fill(void* ptr, ulong fill) {
if (ptr is null)
return false;

auto pd = getPageDescriptor(ptr);
// Not supports slab allocs yet
if (pd.isSlab())
return false;

if (fill <= pd.extent.size) {
pd.extent.allocSize = fill;
return true;
}

return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API has numerous problems, a number of which would have easily been detected if we had tests for it. For instance, what happens when it is called on a pointer that isn't managed by the GC? Or what about interior pointers?

Consider:

uint[] slice = (new uint[123])[0 .. 123];
slice = slice[10 .. $];
slice ~= 42;

If there is room, there is no reason for this to reallocate.

Having tests that exercise the API to achieve this kind of result would tell you if the current API is the right one or not. I predict there will be superfluous methods in there, and that the remaining one will have to be amended substantially.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I MR'd this prematurely, apologies.

@deadalnix
Copy link
Contributor

Also title doesn't really match what this does, and there is no description whatsoever...

@dsm9000 dsm9000 changed the title bare-bones appendables Appendability for large allocs. Aug 15, 2023
@dsm9000 dsm9000 marked this pull request as ready for review August 16, 2023 13:57
Copy link
Contributor

@deadalnix deadalnix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is going to make progress, this must start from the use case. Building a bunch of feature really isn't improving the code if these feature do not add value in some way. In fact, it makes the code bigger, more complex, probably buggier or slower.

Bitmap!512 _slabData;

// Metadata for non-slab (large) size classes
size_t allocSize; // Actual alloc size, 0 if not appendable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not put comment on the same line as declarations.

@@ -202,6 +227,10 @@ public:
return ec.isSlab();
}

bool isLarge() const {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that in the middle of slab features? Make a large features section and put the large feature related stuff in it.

@@ -99,7 +99,15 @@ private:
Links _links;

import d.gc.bitmap;
Bitmap!512 _slabData = void;
union Meta {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metadata

Bitmap!512 _slabData = void;
union Meta {
// Slab occupancy (constrained by freeSlots, so usable for all classes)
Bitmap!512 _slabData;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underscore isn't necessary here anymore.

auto copySize = size;
auto pd = getPageDescriptor(ptr);

// TODO: should realloc work with pointers outside of GC?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would it know how big the previous allocation is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It couldn't. That comment should go.

}

// Get appendable extent of ptr, or null if this is impossible
Extent* maybeGetAppendableExtent(void* ptr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this method necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed because any invocation of appendable API could have been given a non-appendable (slab alloc, or pointer unknown to GC.)

@@ -54,6 +64,39 @@ public:
pd.arena.free(emap, pd, ptr);
}

// Determine whether given alloc is appendable
bool is_appendable(void* ptr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not used anywhere, and is just a helper around another function, that doesn't seems worth including.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intended to be part of the exported API for appendables, rather than a helper.
What precisely do we intend to do with appendables anyway? This was not discussed yet.


// Get the current fill of an appendable alloc.
// If the alloc is not appendable, returns 0.
size_t get_appendable_fill(void* ptr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work with appendable pointer. This API is still a more or less random set of features that do not seems to be connected with the actual use case.

The actual use case is that D has slices, and slices can be appended to, and reallocations needs to be avoided when unnecessary.

I don't know how to do any of this with the current API, considering it doesn't work with interior pointers.

So, what happens when I allocate a slice? What happens when I want to append to a slice? What happens when the slice is not on the GC heap? This is what matters here. I have no API do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I interpreted #266 as concerning the GC strictly, rather than connecting the mechanism with the rest of SDC. This may be the root of our misunderstanding and the reason you've replied with "random set of features that do not seems to be connected" to all of my PR. Could I persuade you to fully describe the requirements for satisfying #266 ? Slices, for instance, were mentioned nowhere in #266.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The API of the GC needs to be extended with a method that allows us to query the size stored via the appendable mechanism."

This is all that was clear from #266 as originally given.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about slices: as I understand, if we're doing an append operation on a slice, we'll get the pointer to the parent array from the GC heap (or from the calling code, which already has it, when we're not doing so from the GC.)


@property
ulong allocSize() {
return isLarge() ? _meta.allocSize : 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It clearly a bug to call this on slabs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well no, because e.g. is_appendable and the related API functions can very well end up being called on a slab alloc, and must return the correct answer (i.e. that the alloc is not appendable, and appendability operations are prohibited.)

sdlib/d/gc/tcache.d Outdated Show resolved Hide resolved
@dsm9000 dsm9000 marked this pull request as draft August 21, 2023 04:10
Copy link
Contributor

@deadalnix deadalnix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allocSize/fill/capacity lingo is very confused and confusing. This is usually a tell that the design is more complex than it needs to be.

It seems to me that this would benefit from focusing on storing and keeping track of the actual allocation size, in addition of the size of the extent itself, and maintaining the right numbers accross various operation such as realloc.

That is clearly a problem that is probably somewhat solved in this PR (although i think it is not correct), and needs to be solved in the PR, so let's build something solid we can build on.

// Get the capacity of the array slice denoted by slice and length.
// It includes the space occupied by the slice itself.
size_t getArrayCapacity(void* sliceAddr, size_t sliceLen) {
if ((sliceAddr is null) || (!sliceLen))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would a slice of length 0 would necessarily have 0 capacity? Also, I don't see why you need to special case null, as the page descriptor will be empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slice of length 0 has capacity of 0. See here.


// Get the capacity of the array slice denoted by slice and length.
// It includes the space occupied by the slice itself.
size_t getArrayCapacity(void* sliceAddr, size_t sliceLen) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GC has no notion of array, only allocation, and this is better this way, IMO. You have allocation, and you have the ability to extend some allocation to fit more in them. The rest doesn't have to be handled by the GC, but can be handled by the rest of the runtime (and is largely out of scope here).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API functions deal on the level of bytes strictly, the array terminology may be out of place here.


auto pd = maybeGetPageDescriptor(sliceAddr);
// Return zero if slice is unknown to GC or is to a non-appendable block:
if ((pd.extent is null) || (!pd.extent.allocSize))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to check if the alloc size is zero, as the next check will fail anyway.

return 0;

// Otherwise, capacity is the block size minus the slice length:
return pd.extent.size - sliceLen;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect, the slice doesn't have to start at the beginning of the extent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That isn't what we're checking here. Per the D docs, slice has to end at the same place as the valid data in order to have a capacity.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clearly wrong. Remove everything else, write the proper tests, and it'll be obvious in no time.

// Append array slice denoted by rAddr and rBytes to the one
// denoted by lAddr and lBytes, using the former's appendable capacity
// if possible, otherwise allocating a new array.
void* appendArray(void* lAddr, size_t lBytes, void* rAddr, size_t rBytes) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the wrong abstraction level, once again. the GC could not care less about arrays. The runtime does. What you want is to be able to extend an existing allocation.

There is no reason for the GC itself to do memcopies and whatnot. All of that is language level semantic that have nothing to do in there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the memcopies etc. were to take place outside of the GC, the latter will need to export an API for the containsPointers logic and so forth. Do we want this? IMHO "append in place if possible, otherwise reallocate" belongs in the same place as alloc et al.

if (alignUp(size, PageSize) == esize) {
oldFill = pd.extent.allocSize;

// Prohibit resize below appendable fill:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a sensible design decision.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't it? If we don't have a stored fill, we have to copy the entire alloc space when reallocing.


auto oldSize = oldFill ? oldFill : pd.extent.size;

if (alignUp(size, PageSize) == alignUp(oldSize, PageSize)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are short circuiting the whole thing when the size you'd get is the same. Yet, later on, you recompute the size.

}

containsPointers = (containsPointers | pd.containsPointers) != 0;
auto newPtr = alloc(size, containsPointers);
auto useSize = oldFill ? upsizeToLarge(size) : size;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you recompute the size, so the previous short circuit is incorrect. This will also lead to quadratic behavior as the size will only be bumped up one page at the time instead of exponentially, which is linear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of bump increment do we want for realloc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incidentally upsizeToLarge does not recompute the size unless the offered size is below the minimum for a large alloc. The size passed to alloc still needs to be aligned.


assert(!getCap(a));
assert(!getCap(b));
assert(!getCap(c));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capacity is an integer. If you expect it to be 0, then check for that.

@dsm9000 dsm9000 marked this pull request as ready for review August 22, 2023 19:36
Copy link
Contributor

@deadalnix deadalnix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are still in complexity land here. This has a few side effects:

  • It becomes difficult to convince oneself that all bases are covered by tests (and in fact, it doesn't looks like this is the case here).
  • The amount of branches grows and grows. For instance, excluding gating, realloc has 5 branches. This feature adds 3 (!!). alloc had 1 branch outside gating, now it has 2 more (!!) This is bad for perfs, testability, understandability, ...
  • API become nonsensical, with conditionally used arguments, or magic behavior in realloc. One pretty much has to know what the implementation of these method is to know what to expect from them, but this cannot scale to any program other than trivially small ones.

/**
* Reallocation.
*/

void* realloc(void* ptr, size_t size, bool containsPointers) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the C API together.

Comment on lines 19 to 20
void* alloc(size_t size, bool containsPointers, bool isAppendable = false,
size_t spareCapacity = 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact spareCapacity is only used in one branch is a good sign that this API is not the right one.

}
void* alloc(size_t size, bool containsPointers, bool isAppendable = false,
size_t spareCapacity = 0) {
// spareCapacity is ignored if alloc is not appendable.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See you noticed it. The model is wrong. Don't say it's wrong in a comment, make it right.

@@ -43,6 +43,7 @@ enum ClassCount {
enum SizeClass {
Tiny = getSizeFromClass(ClassCount.Tiny - 1),
Small = getSizeFromClass(ClassCount.Small - 1),
Large = getSizeFromClass(ClassCount.Small),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semantic of that entry is different. It doesn't belong in there (and this can anyway be computed easily using getAllocSize).

@@ -1,5 +1,6 @@
module d.gc.tcache;

import d.gc.extent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Comment on lines 415 to 417
// Enlarging the spare capacity :
p0 = threadCache.realloc(p0, 16385, false);
assert(threadCache.getCapacity(p0, 100) == 36864);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be out of scope in general, but holly molly, an increase by a factor of almost 3x? That doesn't seem reasonable at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What multiplier do you consider reasonable here? Is 2x reasonable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The size classes already provide what's needed.

Nevertheless it is out of scope for the PR.

Comment on lines 419 to 425
// Reduce again to minimum:
p0 = threadCache.realloc(p0, 100, false);
assert(threadCache.getCapacity(p0, 100) == 16384);

// Enlarge the empty (will double in total capacity) :
p2 = threadCache.realloc(p2, 16385, false);
assert(threadCache.getCapacity(p2, 0) == 32768);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At no point do you test the case for which the realloc is done to a size smaller than the capacity, which definitively should impact the capacity tracking, consider realloc's semantic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

	// This realloc will have no effect, as the requested size
	// is below the appendable fill size:
	auto p3 = threadCache.realloc(p0, 99, false);
	assert(p3 == p0);

Again, under what circumstances would it be correct to permit this??

Comment on lines 396 to 397
// Zero-length segment of alloc where data exists in front of it:
assert(threadCache.getCapacity(p0, 0) == 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The obvious missing case is length 0 at the end of the segment.

Comment on lines 160 to 175
// New block will be appendable if the old one was:
appendable = pd.extent.isAppendable();

// TODO: Try to extend/shrink in place.
import d.gc.util;
copySize = min(size, esize);
if (appendable) {
// Prohibit resize below appendable fill:
if (size < pd.extent.allocSize)
return ptr;
copySize = pd.extent.allocSize;
allocateSize = copySize;
// If enlarging an appendable, boost spare capacity:
if (esize < size)
spareCapacity = max(size, esize * 2);
} else {
copySize = min(size, esize);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this doesn't seem to relate to keeping track of capacity.

Comment on lines 165 to 167
// Prohibit resize below appendable fill:
if (size < pd.extent.allocSize)
return ptr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under what circumstances would one want to realloc below the size of the live data?!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know, maybe because the data is not needed anymore. What matter is that we were asked to.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accumulating more and more special cases trying to guess what to do in each instance is the recipe for exploding complexity.

If we are requested to reallocate into something smaller, we ought to do it. That's really all there is to consider.

@deadalnix deadalnix force-pushed the master branch 2 times, most recently from f94baa8 to 87a9baf Compare August 23, 2023 22:14
@dsm9000 dsm9000 marked this pull request as draft August 23, 2023 23:29
@dsm9000 dsm9000 marked this pull request as ready for review August 24, 2023 00:08
@deadalnix
Copy link
Contributor

Please update title/description and this can go in. Next step is extend.

@dsm9000 dsm9000 changed the title Appendability for large allocs. allocAppendable() and getCapacity(). Aug 24, 2023
@deadalnix deadalnix merged commit 0196479 into snazzy-d:master Aug 24, 2023
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants