From b4f2f4c3bb859478436486b3cf0238b72a1f888a Mon Sep 17 00:00:00 2001 From: Eddie Date: Wed, 27 Nov 2024 19:02:21 -0800 Subject: [PATCH 01/16] Initial --- benchmark/CMakeLists.txt | 23 ++++ benchmark/main.catch2.cpp | 3 + benchmark/str-experiment/zoo/Str.h | 198 +++++++++++++++++++++++++++++ benchmark/string.catch2.cpp | 51 ++++++++ 4 files changed, 275 insertions(+) create mode 100644 benchmark/main.catch2.cpp create mode 100644 benchmark/str-experiment/zoo/Str.h create mode 100644 benchmark/string.catch2.cpp diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 5bd8337b..0bef138b 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -73,3 +73,26 @@ target_link_libraries( zoo-c_str-implementations benchmark::benchmark ) + +# Generated by ChatGPT o1-preview +add_executable( + catch2StringTest + main.catch2.cpp string.catch2.cpp +) + +set_xcode_properties(catch2StringTest) + +target_include_directories( + catch2StringTest PRIVATE + str-experiment + ../test/inc + ../inc + ../junkyard/inc + ../test/third_party/Catch2/single_include +) + +set_target_properties(catch2StringTest PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) diff --git a/benchmark/main.catch2.cpp b/benchmark/main.catch2.cpp new file mode 100644 index 00000000..f481b16f --- /dev/null +++ b/benchmark/main.catch2.cpp @@ -0,0 +1,3 @@ +#define CATCH_CONFIG_MAIN + +#include diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h new file mode 100644 index 00000000..27bfdb5a --- /dev/null +++ b/benchmark/str-experiment/zoo/Str.h @@ -0,0 +1,198 @@ +#include +#include // memcpy +#include // aligned allocations +//#include +#include + +namespace zoo { + +template +constexpr auto Log2Floor(T value) { + return sizeof(unsigned long long) * 8 - 1 - + //std::countl_zero(value); + __builtin_clzll(value); +} + +template +// assumes 1 < value! +constexpr auto Log2Ceiling(T value) { + return 1 + Log2Floor(value - 1); +} + +inline void noOp(...) {} +#define printf noOp +#define fprintf noOp + +/// \brief Ah +/// +/// Design: +/// To minimize allocations, this type will attempt to hold the chars +/// of the string locally. +/// To maximize space efficiency, the encoding of whether the string +/// is local or on the heap will occur at the last byte, least +/// significant bits of the local storage. +/// the local storage doubles as the encoding of a heap pointer, for +/// strings that can not be held locally +/// This uses Andrei Alexandrescu's idea of storing the size of the +/// string as the remaining local capacity, in this way, a 0 local +/// capacity also encodes the null byte for c_str(), as reported by +/// Nicholas Omrod at CPPCon 2016. +/// In case the string is too long for local allocation, the +/// pointer in the heap will be encoded locally in the last word +/// of the representation. +/// \tparam StorageModel Indicates the size of the local storage +/// and its alignment +/// For large strings (allocated in the heap): +/// In the last void * worth of local storage: +/// +--------- Big Endian diagram ------------------------ +/// | Most significant part of Pointer | BytesForSizeEncoding +/// +--------- Little Endian actual representation +/// For local strings: +/// Little Endian diagram +/// +----------------------------------------------------- +/// | characters... | remaining count +/// +------------------------------ +/// The last byte looks like this: +/// For heap encoding: number of bytes of the size in the allocation, +/// the value is in the interval [1, 8], this is represented with 4 +/// bits in which the bit for +/// For local encoding there is the need for log2(Size) bits to express +/// the size of the local string (either as size or remaining) +/// And we need one more bit to encode local (zero) or heap(1) +template +struct Str { + constexpr static auto + Size = sizeof(StorageModel), // remaining count is Size - used + BitsToEncodeLocalLength = Log2Ceiling(Size - 1), + CodeSize = BitsToEncodeLocalLength + 1, + HeapAlignmentRequirement = std::size_t(1) << CodeSize; + ; + constexpr static char OnHeapIndicatorFlag = 1 << BitsToEncodeLocalLength; + static_assert(sizeof(void *) <= Size); + alignas(sizeof(StorageModel)) char buffer_[sizeof(StorageModel)]; + + auto &lastByte() const { return buffer_[Size - 1]; } + auto &lastByte() { return buffer_[Size - 1]; } + auto lastPtr() const { return buffer_ + Size - sizeof(char *); } + auto lastPtr() { return buffer_ + Size - sizeof(char *); } + auto allocationPtr() const { + uintptr_t codeThatContainsAPointer; + memcpy(&codeThatContainsAPointer, lastPtr(), sizeof(char *)); + assert(onHeap()); + auto asLittleEndian = __builtin_bswap64(codeThatContainsAPointer); + auto bytesForSizeMinus1 = asLittleEndian & 7; + auto codeRemoved = + asLittleEndian ^ OnHeapIndicatorFlag ^ bytesForSizeMinus1; + + char *rv; + memcpy(&rv, &codeRemoved, sizeof(char *)); + return rv; + } + + Str() { + buffer_[0] = '\0'; + lastByte() = Size - 1; + } + + + Str(const char *source, std::size_t length) { + if(length <= Size) { + lastByte() = Size - length; + memcpy(buffer_, source, length); + } else { + auto bytesForSize = (Log2Ceiling(length) + 7) >> 3; + auto onHeap = + new(std::align_val_t(HeapAlignmentRequirement)) + char[bytesForSize + length]; + fprintf(stderr, "onHeap: %p\n", onHeap); + + assert(onHeap); + // assumes little endian here! + memcpy(onHeap, &length, bytesForSize); + memcpy(onHeap + bytesForSize, source, length); + + uintptr_t littleEndian; + memcpy(&littleEndian, &onHeap, sizeof(char *)); + auto bigEndian = __builtin_bswap64(littleEndian); + memcpy(lastPtr(), &bigEndian, sizeof(char *)); + // We encode the range [1..8] as [0..7] + auto code = (bytesForSize - 1) | OnHeapIndicatorFlag; + fprintf( + stderr, + "LB before: %x, code %x\n", + (unsigned char)lastByte(), + (unsigned char) code + ); + + lastByte() |= code; + fprintf(stderr, "LB after: %x\n", (unsigned char)lastByte()); + assert(onHeap); + printf("In ctor: lastByte(): %02x\n", (unsigned char)lastByte()); + printf("length: %lu, bytesForSize: %lu\n", length, bytesForSize); + printf("OnHeapIndicatorFlag: %02x\n", OnHeapIndicatorFlag); + + printf("buffer_ : %p:\n", buffer_); + for (auto i = 0; i < Size; i++) { + printf("%02x ", (unsigned char)buffer_[i]); + } + printf("\n"); + auto ptrToStr = allocationPtr() + bytesForSize; + printf("%p \n", ptrToStr); + printf("\n"); + + for (auto i = 0; i <= length; i++) { + printf("%c ", (unsigned char)ptrToStr[i]); + } + printf("\n"); + + //assert('\0' == ptrToStr[length]); + + printf("Size: %lu\n", Size); + printf("BitsToEncodeLocalLength: %lu\n", BitsToEncodeLocalLength); + printf("CodeSize: %lu\n", CodeSize); + printf("HeapAlignmentRequirement: %lu\n", HeapAlignmentRequirement); + } + } + + auto size() const noexcept { + if(onHeap()) { + auto countOfBytesForEncodingSize = 1 + (lastByte() & 7); + const char *ptr = allocationPtr(); + std::remove_const_t length = 0; + memcpy(&length, ptr, countOfBytesForEncodingSize); + return length - 1; + } + else { + return Size - lastByte() - 1; + } + } + + template + Str(const char (&in)[L]): Str(static_cast(in), L - 1) {} + + + const char *c_str() noexcept { + if (!onHeap()) { + return buffer_; + } + + uintptr_t code; + memcpy(&code, lastPtr(), sizeof(char *)); + + // the three least-significant bits of code contain the size of length, + // encoded as a range [0..7] that maps to a range [1..8] in which each + // the unit is a byte + auto bytesForLength = 1 + (code & 7); + + auto location = allocationPtr(); + auto rv = location + bytesForLength; + fprintf(stderr, "Returning %s\n", rv); + return rv; + } + + auto onHeap() const noexcept { + return bool(OnHeapIndicatorFlag & lastByte()); + } +}; + +} // closes zoo diff --git a/benchmark/string.catch2.cpp b/benchmark/string.catch2.cpp new file mode 100644 index 00000000..f75e920d --- /dev/null +++ b/benchmark/string.catch2.cpp @@ -0,0 +1,51 @@ +#include "zoo/Str.h" + +#include + +TEST_CASE("String", "[str][api]") { + using S = zoo::Str; + SECTION("Potential regression") { + char buff[8] = { "****#!-" }; + char Hi[] = { "Hello" }; + REQUIRE('\0' == buff[7]); + auto inPlaced = new(buff) S(Hi, sizeof(Hi)); + REQUIRE('\0' == buff[5]); + REQUIRE('-' == buff[6]); + REQUIRE('\x2' == buff[7]); + } + SECTION("Empty/defaulted") { + S empty; + CHECK(not empty.onHeap()); + CHECK(0 == empty.size()); + CHECK('\0' == *empty.c_str()); + } + auto checks = + [](auto &givenString, bool onHeap) { + auto facilitateComparisons = std::string(givenString); + S s(givenString, facilitateComparisons.length() + 1); + auto isOnHeap = s.onHeap(); + CHECK(onHeap == isOnHeap); + + auto c_str = s.c_str(); + CHECK(c_str == facilitateComparisons); + CHECK(strlen(c_str) == facilitateComparisons.size()); + auto zooSize = s.size(); + CHECK(zooSize == facilitateComparisons.size()); + }; + SECTION("Local") { + SECTION("Not boundary") { + checks("Hello", false); + } + SECTION("Boundary of all chars needed") { + static_assert(8 == sizeof(S)); + constexpr char Exactly7[] = { "1234567" }; + checks(Exactly7, false); + } + } + SECTION("Heap") { + constexpr char Quixote[] = "\ +En un lugar de la Mancha que no quiero recordar\ +"; + checks(Quixote, true); + } +} From 792fa68b35dca9f5feb963e5757486b15494ef61 Mon Sep 17 00:00:00 2001 From: Eddie Date: Wed, 27 Nov 2024 19:39:58 -0800 Subject: [PATCH 02/16] Fixes c_str --- benchmark/str-experiment/zoo/Str.h | 7 ++----- benchmark/string.catch2.cpp | 9 +++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h index 27bfdb5a..d5f7f9d1 100644 --- a/benchmark/str-experiment/zoo/Str.h +++ b/benchmark/str-experiment/zoo/Str.h @@ -126,7 +126,6 @@ struct Str { lastByte() |= code; fprintf(stderr, "LB after: %x\n", (unsigned char)lastByte()); - assert(onHeap); printf("In ctor: lastByte(): %02x\n", (unsigned char)lastByte()); printf("length: %lu, bytesForSize: %lu\n", length, bytesForSize); printf("OnHeapIndicatorFlag: %02x\n", OnHeapIndicatorFlag); @@ -175,14 +174,12 @@ struct Str { if (!onHeap()) { return buffer_; } - - uintptr_t code; - memcpy(&code, lastPtr(), sizeof(char *)); // the three least-significant bits of code contain the size of length, // encoded as a range [0..7] that maps to a range [1..8] in which each // the unit is a byte - auto bytesForLength = 1 + (code & 7); + auto byteWithEncoding = lastByte(); + auto bytesForLength = 1 + (byteWithEncoding & 7); auto location = allocationPtr(); auto rv = location + bytesForLength; diff --git a/benchmark/string.catch2.cpp b/benchmark/string.catch2.cpp index f75e920d..fdeeebaa 100644 --- a/benchmark/string.catch2.cpp +++ b/benchmark/string.catch2.cpp @@ -2,6 +2,14 @@ #include +constexpr char chars257[] = "\ +0123456789ABCDEF0123456789abcdef0123456789ABCDEF0123456789abcdef\ +we can put anything here, since we can see it is 64-bytes wide--\ +continuing, spaces are also good -\ +this is the last line !!\ +"; +static_assert(257 == sizeof(chars257)); + TEST_CASE("String", "[str][api]") { using S = zoo::Str; SECTION("Potential regression") { @@ -47,5 +55,6 @@ TEST_CASE("String", "[str][api]") { En un lugar de la Mancha que no quiero recordar\ "; checks(Quixote, true); + checks(chars257, true); } } From 8b0ca1c5faa10d1121028ba5baac67b44d091c87 Mon Sep 17 00:00:00 2001 From: Eddie Date: Wed, 27 Nov 2024 20:02:03 -0800 Subject: [PATCH 03/16] Destructor, noexceptness --- benchmark/str-experiment/zoo/Str.h | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h index d5f7f9d1..4b467ce4 100644 --- a/benchmark/str-experiment/zoo/Str.h +++ b/benchmark/str-experiment/zoo/Str.h @@ -71,11 +71,11 @@ struct Str { static_assert(sizeof(void *) <= Size); alignas(sizeof(StorageModel)) char buffer_[sizeof(StorageModel)]; - auto &lastByte() const { return buffer_[Size - 1]; } - auto &lastByte() { return buffer_[Size - 1]; } - auto lastPtr() const { return buffer_ + Size - sizeof(char *); } - auto lastPtr() { return buffer_ + Size - sizeof(char *); } - auto allocationPtr() const { + auto &lastByte() const noexcept { return buffer_[Size - 1]; } + auto &lastByte() noexcept { return buffer_[Size - 1]; } + auto lastPtr() const noexcept { return buffer_ + Size - sizeof(char *); } + auto lastPtr() noexcept { return buffer_ + Size - sizeof(char *); } + auto allocationPtr() const noexcept { uintptr_t codeThatContainsAPointer; memcpy(&codeThatContainsAPointer, lastPtr(), sizeof(char *)); assert(onHeap()); @@ -89,12 +89,11 @@ struct Str { return rv; } - Str() { + Str() noexcept { buffer_[0] = '\0'; lastByte() = Size - 1; } - Str(const char *source, std::size_t length) { if(length <= Size) { lastByte() = Size - length; @@ -143,8 +142,6 @@ struct Str { printf("%c ", (unsigned char)ptrToStr[i]); } printf("\n"); - - //assert('\0' == ptrToStr[length]); printf("Size: %lu\n", Size); printf("BitsToEncodeLocalLength: %lu\n", BitsToEncodeLocalLength); @@ -167,8 +164,9 @@ struct Str { } template - Str(const char (&in)[L]): Str(static_cast(in), L - 1) {} - + Str(const char (&in)[L]) noexcept(L <= Size): + Str(static_cast(in), L - 1) + {} const char *c_str() noexcept { if (!onHeap()) { @@ -187,6 +185,10 @@ struct Str { return rv; } + ~Str() { + if(onHeap()) { delete[] allocationPtr(); } + } + auto onHeap() const noexcept { return bool(OnHeapIndicatorFlag & lastByte()); } From 591960d4921f203cee77c6c10408a36d0e7fa493 Mon Sep 17 00:00:00 2001 From: Eddie Date: Sat, 30 Nov 2024 11:23:32 -0800 Subject: [PATCH 04/16] refactoring --- benchmark/str-experiment/zoo/Str.h | 120 +++++++++++++++++++---------- benchmark/string.catch2.cpp | 12 ++- 2 files changed, 89 insertions(+), 43 deletions(-) diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h index 4b467ce4..5db46454 100644 --- a/benchmark/str-experiment/zoo/Str.h +++ b/benchmark/str-experiment/zoo/Str.h @@ -89,9 +89,32 @@ struct Str { return rv; } - Str() noexcept { - buffer_[0] = '\0'; - lastByte() = Size - 1; + auto encodePointer(char numberOfBytes, const char *ptr) { + uintptr_t code; + memcpy(&code, &ptr, sizeof(char *)); + static_assert(8 == sizeof(char *)); + if(8 < numberOfBytes) { + __builtin_unreachable(); + } + return + __builtin_bswap64(code | (numberOfBytes - 1) | OnHeapIndicatorFlag); + } + + auto size() const noexcept { + if(onHeap()) { + auto countOfBytesForEncodingSize = 1 + (lastByte() & 7); + const char *ptr = allocationPtr(); + std::remove_const_t length = 0; + memcpy(&length, ptr, countOfBytesForEncodingSize); + return length - 1; + } + else { + return Size - lastByte() - 1; + } + } + + auto onHeap() const noexcept { + return bool(OnHeapIndicatorFlag & lastByte()); } Str(const char *source, std::size_t length) { @@ -110,25 +133,11 @@ struct Str { memcpy(onHeap, &length, bytesForSize); memcpy(onHeap + bytesForSize, source, length); - uintptr_t littleEndian; - memcpy(&littleEndian, &onHeap, sizeof(char *)); - auto bigEndian = __builtin_bswap64(littleEndian); - memcpy(lastPtr(), &bigEndian, sizeof(char *)); + auto code = encodePointer(bytesForSize, onHeap); + auto addressOfLastPointerWorthOfSpace = lastPtr(); + memcpy(addressOfLastPointerWorthOfSpace, &code, sizeof(char *)); // We encode the range [1..8] as [0..7] - auto code = (bytesForSize - 1) | OnHeapIndicatorFlag; - fprintf( - stderr, - "LB before: %x, code %x\n", - (unsigned char)lastByte(), - (unsigned char) code - ); - - lastByte() |= code; - fprintf(stderr, "LB after: %x\n", (unsigned char)lastByte()); - printf("In ctor: lastByte(): %02x\n", (unsigned char)lastByte()); - printf("length: %lu, bytesForSize: %lu\n", length, bytesForSize); - printf("OnHeapIndicatorFlag: %02x\n", OnHeapIndicatorFlag); - + printf("buffer_ : %p:\n", buffer_); for (auto i = 0; i < Size; i++) { printf("%02x ", (unsigned char)buffer_[i]); @@ -150,24 +159,59 @@ struct Str { } } - auto size() const noexcept { - if(onHeap()) { - auto countOfBytesForEncodingSize = 1 + (lastByte() & 7); - const char *ptr = allocationPtr(); - std::remove_const_t length = 0; - memcpy(&length, ptr, countOfBytesForEncodingSize); - return length - 1; - } - else { - return Size - lastByte() - 1; - } - } - template Str(const char (&in)[L]) noexcept(L <= Size): Str(static_cast(in), L - 1) {} + Str() noexcept { + buffer_[0] = '\0'; + lastByte() = Size - 1; + } + + Str(const Str &model): Str(model.c_str(), model.size() + 1) {} + + Str(Str &&donor) noexcept { + memcpy(buffer_, donor.buffer_, sizeof(this->buffer_)); + donor.lastByte() = ~OnHeapIndicatorFlag; + } + + Str &operator=(const Str &model) { + if(!model.onHeap()) { + if(onHeap()) { delete[] allocationPtr(); } + else { + if(this == &model) { return *this; } + } + new(this) Str(model); + return *this; + } + // The model is on the heap + auto modelSize = model.size(); + if(size() < modelSize) { + if(onHeap()) { this->~Str(); } + new(this) Str(model); + } else { + if(this == &model) { return *this; } + // assert we are on the heap too + auto modelBytesForSize = 1 + (model.lastByte() & 7); + auto myPtr = allocationPtr(); + auto modelPtr = model.allocationPtr(); + memcpy(myPtr, modelPtr, modelBytesForSize + modelSize + 1); + encodePointer(modelBytesForSize, myPtr); + } + return *this; + }; + + Str &operator=(Str &&donor) noexcept { + Str temporary(std::move(donor)); + this->~Str(); + return *new(this) Str(std::move(temporary)); + } + + ~Str() { + if(onHeap()) { delete[] allocationPtr(); } + } + const char *c_str() noexcept { if (!onHeap()) { return buffer_; @@ -184,14 +228,6 @@ struct Str { fprintf(stderr, "Returning %s\n", rv); return rv; } - - ~Str() { - if(onHeap()) { delete[] allocationPtr(); } - } - - auto onHeap() const noexcept { - return bool(OnHeapIndicatorFlag & lastByte()); - } }; } // closes zoo diff --git a/benchmark/string.catch2.cpp b/benchmark/string.catch2.cpp index fdeeebaa..bf59fe46 100644 --- a/benchmark/string.catch2.cpp +++ b/benchmark/string.catch2.cpp @@ -2,6 +2,17 @@ #include +using S = zoo::Str; + +static_assert(std::is_nothrow_default_constructible_v); + +char BufferAsBigAsStr[sizeof(S)]; +static_assert(noexcept(S{BufferAsBigAsStr})); + +char BufferLargerThanStr[sizeof(S) * 2]; +static_assert(not noexcept(S{BufferLargerThanStr})); +static_assert(std::is_nothrow_move_constructible_v); + constexpr char chars257[] = "\ 0123456789ABCDEF0123456789abcdef0123456789ABCDEF0123456789abcdef\ we can put anything here, since we can see it is 64-bytes wide--\ @@ -11,7 +22,6 @@ this is the last line !!\ static_assert(257 == sizeof(chars257)); TEST_CASE("String", "[str][api]") { - using S = zoo::Str; SECTION("Potential regression") { char buff[8] = { "****#!-" }; char Hi[] = { "Hello" }; From add6f57d72b0983e77e78001a8a95e4a1994aaa0 Mon Sep 17 00:00:00 2001 From: Eddie Date: Sat, 30 Nov 2024 11:56:23 -0800 Subject: [PATCH 05/16] Completes unit tests --- benchmark/string.catch2.cpp | 48 +++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/benchmark/string.catch2.cpp b/benchmark/string.catch2.cpp index bf59fe46..d06d4f51 100644 --- a/benchmark/string.catch2.cpp +++ b/benchmark/string.catch2.cpp @@ -21,6 +21,20 @@ this is the last line !!\ "; static_assert(257 == sizeof(chars257)); +template +auto stringChecks(const char *givenString, bool onHeap) { + auto facilitateComparisons = std::string(givenString); + StringType s(givenString, facilitateComparisons.length() + 1); + auto isOnHeap = s.onHeap(); + CHECK(onHeap == isOnHeap); + + auto c_str = s.c_str(); + CHECK(c_str == facilitateComparisons); + CHECK(strlen(c_str) == facilitateComparisons.size()); + auto zooSize = s.size(); + CHECK(zooSize == facilitateComparisons.size()); +}; + TEST_CASE("String", "[str][api]") { SECTION("Potential regression") { char buff[8] = { "****#!-" }; @@ -37,34 +51,32 @@ TEST_CASE("String", "[str][api]") { CHECK(0 == empty.size()); CHECK('\0' == *empty.c_str()); } - auto checks = - [](auto &givenString, bool onHeap) { - auto facilitateComparisons = std::string(givenString); - S s(givenString, facilitateComparisons.length() + 1); - auto isOnHeap = s.onHeap(); - CHECK(onHeap == isOnHeap); - - auto c_str = s.c_str(); - CHECK(c_str == facilitateComparisons); - CHECK(strlen(c_str) == facilitateComparisons.size()); - auto zooSize = s.size(); - CHECK(zooSize == facilitateComparisons.size()); - }; + constexpr char Exactly7[] = { "1234567" }; + char Exactly15[] = "123456789ABCDEF"; + constexpr char Quixote[] = "\ +En un lugar de la Mancha que no quiero recordar\ +"; + auto *checks = stringChecks; SECTION("Local") { SECTION("Not boundary") { checks("Hello", false); } SECTION("Boundary of all chars needed") { static_assert(8 == sizeof(S)); - constexpr char Exactly7[] = { "1234567" }; checks(Exactly7, false); } } SECTION("Heap") { - constexpr char Quixote[] = "\ -En un lugar de la Mancha que no quiero recordar\ -"; + checks(Exactly15, true); checks(Quixote, true); - checks(chars257, true); + SECTION("Case in which 1 < the bytes for size") { + checks(chars257, true); + } + } + SECTION("Larger than void *") { + using S2 = zoo::Str; + stringChecks(Exactly7, false); + stringChecks(Exactly15, false); + stringChecks(Quixote, true); } } From caf5f0d43e999f121071b25b13cd004ddf88146b Mon Sep 17 00:00:00 2001 From: Eddie Date: Sat, 30 Nov 2024 19:00:16 -0800 Subject: [PATCH 06/16] First rough benchmarks --- benchmark/CMakeLists.txt | 18 ++++ benchmark/onlyMain.benchmark.catch2.cpp | 6 ++ benchmark/str-experiment/zoo/Str.h | 2 +- benchmark/str.benchmark.catch2.cpp | 136 ++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 benchmark/onlyMain.benchmark.catch2.cpp create mode 100644 benchmark/str.benchmark.catch2.cpp diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 0bef138b..c6227cbf 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -96,3 +96,21 @@ set_target_properties(catch2StringTest PROPERTIES CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF ) + +# not yet mergeable with the master branch +add_executable( + str-benchmark.catch2 + onlyMain.benchmark.catch2.cpp + str.benchmark.catch2.cpp +) + +set_xcode_properties(str-benchmark.catch2) + +target_include_directories( + str-benchmark.catch2 PRIVATE + str-experiment + ../test/inc + ../inc + ../junkyard/inc + ../test/third_party/Catch2/single_include +) diff --git a/benchmark/onlyMain.benchmark.catch2.cpp b/benchmark/onlyMain.benchmark.catch2.cpp new file mode 100644 index 00000000..c21dd955 --- /dev/null +++ b/benchmark/onlyMain.benchmark.catch2.cpp @@ -0,0 +1,6 @@ +#define CATCH_CONFIG_ENABLE_BENCHMARKING +#define CATCH_CONFIG_MAIN + +#include + + diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h index 5db46454..4d6eb1b8 100644 --- a/benchmark/str-experiment/zoo/Str.h +++ b/benchmark/str-experiment/zoo/Str.h @@ -212,7 +212,7 @@ struct Str { if(onHeap()) { delete[] allocationPtr(); } } - const char *c_str() noexcept { + const char *c_str() const noexcept { if (!onHeap()) { return buffer_; } diff --git a/benchmark/str.benchmark.catch2.cpp b/benchmark/str.benchmark.catch2.cpp new file mode 100644 index 00000000..825aba28 --- /dev/null +++ b/benchmark/str.benchmark.catch2.cpp @@ -0,0 +1,136 @@ +#include "zoo/Str.h" + +#define CATCH_CONFIG_ENABLE_BENCHMARKING +#include + +#include +#include +#include +#include +#include + +auto loadTextCorpus(std::string_view path) { + std::vector strings; + std::vector ints; + std::unordered_map forwardMap; + int nextIndex = 0; + + std::ifstream text{path}; + if(!text) { abort(); } + std::string line; + std::regex separator{"\\w+"}; + text.clear(); + text.seekg(0); + while(text) { + getline(text, line); + std::sregex_iterator + wordsEnd{}, + wordIterator{line.begin(), line.end(), separator}; + while(wordsEnd != wordIterator) { + const auto &word = wordIterator->str(); + strings.push_back(word); + auto [where, notPresent] = + forwardMap.insert({ word, nextIndex }); + ints.push_back( + notPresent ? nextIndex++ : where->second + ); + ++wordIterator; + } + } + WARN(forwardMap.size() << " different strings identified"); + REQUIRE(ints.size() == strings.size()); + REQUIRE(forwardMap.size() == nextIndex); + return std::tuple(strings, ints); +} + +using ZStr = zoo::Str; + +namespace zoo { + +template +bool operator<( + const Str &left, + const Str &right +) { + auto lS = left.size(), rS = right.size(); + auto minLength = lS < rS ? lS : rS; + auto preRV = memcmp(left.c_str(), right.c_str(), minLength); + return preRV < 0 || (0 == preRV && lS < rS); +} + +} + +TEST_CASE("Str benchmarks") { + auto [strings, integers] = loadTextCorpus( + "/tmp/deleteme/TheTurnOfTheScrew.txt.lowercase.txt" + ); + std::vector zss; + for(auto &source: strings) { + zss.push_back({source.data(), source.size() + 1}); + } + REQUIRE(strings.size() == integers.size()); + REQUIRE(zss.size() == integers.size()); + auto buildHistogram = + [](auto &histogram, const auto &events) { + for(auto &event: events) { + ++histogram[event]; + } + }; + auto &strs = strings; + auto &ints = integers; + + std::map zooH; + std::map intH; + std::map stdH; + + auto hSize = [&](auto &h, auto &series) { + h.clear(); + buildHistogram(h, series); + return h.size(); + }; + auto iS = hSize(intH, ints); + REQUIRE(hSize(zooH, zss) == iS); + + BENCHMARK("zoo::Str") { + zooH.clear(); + buildHistogram(zooH, zss); + return zooH.size(); + }; + BENCHMARK("Baseline") { + intH.clear(); + buildHistogram(intH, ints); + return intH.size(); + }; + BENCHMARK("std::string") { + stdH.clear(); + buildHistogram(stdH, strs); + return stdH.size(); + }; + BENCHMARK("zoo::Str 2") { + zooH.clear(); + buildHistogram(zooH, zss); + return zooH.size(); + }; + std::map, int> zooTwoPointersH; + std::vector> twoPSeries; + for(auto &source: strings) { + twoPSeries.push_back({ source.data(), source.size() + 1 }); + } + BENCHMARK("zoo::Str") { + zooTwoPointersH.clear(); + buildHistogram(zooTwoPointersH, twoPSeries); + return zooTwoPointersH.size(); + }; + + std::map, int> zooTwoPointersH8; + std::vector> twoPSeries8; + for(auto &source: strings) { + twoPSeries8.push_back({ source.data(), source.size() + 1 }); + } + BENCHMARK("zoo::Str") { + zooTwoPointersH8.clear(); + buildHistogram(zooTwoPointersH8, twoPSeries8); + return zooTwoPointersH8.size(); + }; + +} From d6e27fe58e0c6bf94632d8cff377e4939277e291 Mon Sep 17 00:00:00 2001 From: Eddie Date: Sat, 30 Nov 2024 21:00:07 -0800 Subject: [PATCH 07/16] Potential fix to Xcode c++ standard, avx2 --- benchmark/CMakeLists.txt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index c6227cbf..8a8824c7 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -15,15 +15,9 @@ if(CMAKE_BUILD_TYPE MATCHES "Release|RelWithDebInfo") endif() endif() -# Macro to set properties for Xcode targets macro(set_xcode_properties TARGET_NAME) - if(CMAKE_GENERATOR STREQUAL Xcode) - set_target_properties(${TARGET_NAME} PROPERTIES - XCODE_ATTRIBUTE_ENABLE_AVX YES - XCODE_ATTRIBUTE_ENABLE_AVX2 YES - XCODE_ATTRIBUTE_OTHER_CPLUSPLUSFLAGS "-mavx -mavx2 -mbmi2" - XCODE_ATTRIBUTE_OTHER_CFLAGS "-mavx -mavx2 -mbmi2" - ) + if(CMAKE_GENERATOR STREQUAL Xcode AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") + target_compile_options(${TARGET_NAME} PRIVATE -mavx -mavx2 -mbmi2) endif() endmacro() @@ -91,11 +85,6 @@ target_include_directories( ../test/third_party/Catch2/single_include ) -set_target_properties(catch2StringTest PROPERTIES - CXX_STANDARD 20 - CXX_STANDARD_REQUIRED ON - CXX_EXTENSIONS OFF -) # not yet mergeable with the master branch add_executable( From 17df30f9cef563608342cb476ce2d1ce60be4437 Mon Sep 17 00:00:00 2001 From: Eddie Date: Mon, 2 Dec 2024 13:24:18 -0800 Subject: [PATCH 08/16] Alignment does not have to be to the size of the prototype, just its alignment --- benchmark/str-experiment/zoo/Str.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h index 4d6eb1b8..b23caab3 100644 --- a/benchmark/str-experiment/zoo/Str.h +++ b/benchmark/str-experiment/zoo/Str.h @@ -19,6 +19,11 @@ constexpr auto Log2Ceiling(T value) { return 1 + Log2Floor(value - 1); } +template +constexpr auto Log256Celing(T value) { + return (7 + Log2Ceiling(value)) / 8; +} + inline void noOp(...) {} #define printf noOp #define fprintf noOp @@ -69,7 +74,7 @@ struct Str { ; constexpr static char OnHeapIndicatorFlag = 1 << BitsToEncodeLocalLength; static_assert(sizeof(void *) <= Size); - alignas(sizeof(StorageModel)) char buffer_[sizeof(StorageModel)]; + alignas(alignof(StorageModel)) char buffer_[sizeof(StorageModel)]; auto &lastByte() const noexcept { return buffer_[Size - 1]; } auto &lastByte() noexcept { return buffer_[Size - 1]; } From f3f28721055a830e179695fcb73638baad847529 Mon Sep 17 00:00:00 2001 From: Eddie Date: Mon, 2 Dec 2024 13:24:39 -0800 Subject: [PATCH 09/16] Efficiency exploration --- benchmark/str.benchmark.catch2.cpp | 137 +++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/benchmark/str.benchmark.catch2.cpp b/benchmark/str.benchmark.catch2.cpp index 825aba28..0768edb6 100644 --- a/benchmark/str.benchmark.catch2.cpp +++ b/benchmark/str.benchmark.catch2.cpp @@ -43,6 +43,65 @@ auto loadTextCorpus(std::string_view path) { return std::tuple(strings, ints); } +template +auto allocationPtr(const STR &c) { + return c.c_str(); +} +template +auto allocationPtr(const zoo::Str &c) { + return c.allocationPtr(); +} + +template +auto onHeap(const STR &c) { + auto cStr = c.c_str(); + uintptr_t asInteger; + memcpy(&asInteger, &cStr, sizeof(char *)); + constexpr auto StringTypeSize = sizeof(STR); + auto baseAddress = &c; + uintptr_t baseAddressAsInteger; + memcpy(&baseAddressAsInteger, &baseAddress, sizeof(char *)); + if( + baseAddressAsInteger <= asInteger && + asInteger < baseAddressAsInteger + StringTypeSize + ) { return false; } + return true; +} + +template +auto onHeap(const zoo::Str &c) { + return c.onHeap(); +} + +template +struct IsZooStr: std::false_type {}; +template +struct IsZooStr>: std::true_type {}; + +static_assert(IsZooStr>::value); +static_assert(!IsZooStr::value); + +template +auto minimumUsedBytes(const STR &s) { + if(!onHeap(s)) { + return sizeof(STR); + } + auto where = allocationPtr(s); + uintptr_t asInt; + memcpy(&asInt, &where, sizeof(char *)); + auto trailingZeroes = __builtin_ctzll(asInt); + auto impliedAllocationSize = 1 << trailingZeroes; + auto roundSizeToAllocationSizeMask = impliedAllocationSize - 1; + auto size = s.size() + 1; + if constexpr(IsZooStr::value) { + size += zoo::Log256Celing(size); + } + // example, allocation size is 64, the mask is then 0b1.1111 (5 bits) + // and size is 17: that's 0b1.0001, the bytes 18 to 63 are not used, + // so, the bytes that this code takes are 63 + return size | roundSizeToAllocationSizeMask; +} + using ZStr = zoo::Str; namespace zoo { @@ -60,6 +119,84 @@ bool operator<( } +#include +// by ChatGPT +std::string toEngineeringString(double value, int precision) { + if (value == 0.0) { + return "0.0"; + } + + int exponent = static_cast(std::floor(std::log10(std::abs(value)))); + int engineeringExponent = exponent - (exponent % 3); // Align to multiple of 3 + double scaledValue = value / std::pow(10, engineeringExponent); + + // Use a stringstream for formatting + std::ostringstream oss; + oss << std::fixed << std::setprecision(precision); + oss << scaledValue << "e" << engineeringExponent; + + return oss.str(); +} + +TEST_CASE("Efficiency counters") { + auto [strs, _] = loadTextCorpus( + "/tmp/deleteme/TheTurnOfTheScrew.txt.lowercase.txt" + ); + constexpr char LongString[] = + "A very long string, contents don't matter, just size"; + SECTION("minimum tests") { + REQUIRE(sizeof(std::string) == minimumUsedBytes(std::string("Hola"))); + REQUIRE(sizeof(std::string) < minimumUsedBytes(std::string(LongString))); + } + strs.push_back(LongString); + + auto process = [](auto &strings) { + auto + allocationCount = 0, + significantBytes = 0, + totalBytesCommitted = 0; + auto stringCount = strings.size() + 1; + for(auto &s: strings) { + if(onHeap(s)) { ++allocationCount; } + significantBytes += s.size(); + totalBytesCommitted += minimumUsedBytes(s); + } + auto average = [stringCount](double v) { + return toEngineeringString(v / stringCount, 3); + }; + WARN( +sizeof(typename std::remove_reference_t::value_type) << ' ' << typeid(decltype(strings)).name() << +"\nCount: " << stringCount << +"\nAllocations: " << allocationCount << +"\nSignificant Bytes: " << significantBytes << +"\nTotalBytes: " << totalBytesCommitted << +"\nEfficiency: " << + toEngineeringString(significantBytes/double(totalBytesCommitted), 3) << +"\nAverages (allocations, size)" << + average(allocationCount) << ' ' << average(significantBytes) << +"\n" + ); + }; + SECTION("std") { + process(strs); + } + + #define QUOTE(a) #a + #define STRINGIFY(a) QUOTE(a) + #define STORAGE_PROTOTYPE_X_LIST \ + X(void *)\ + X(void *[2])\ + X(void *[3])\ + X(void *[4]) + #define X(prototype) \ + SECTION("zoo::Str<" STRINGIFY(prototype) ">") { \ + std::vector> zoos; \ + for(auto &s: strs) { zoos.emplace_back(s.c_str(), s.size() + 1); } \ + process(zoos); \ + } + STORAGE_PROTOTYPE_X_LIST +} + TEST_CASE("Str benchmarks") { auto [strings, integers] = loadTextCorpus( "/tmp/deleteme/TheTurnOfTheScrew.txt.lowercase.txt" From 88c2e60d34b2c19d727313dea1f9859d46d031a0 Mon Sep 17 00:00:00 2001 From: Eddie Date: Mon, 2 Dec 2024 13:40:54 -0800 Subject: [PATCH 10/16] Did not account for the controlling type itself --- benchmark/str.benchmark.catch2.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/str.benchmark.catch2.cpp b/benchmark/str.benchmark.catch2.cpp index 0768edb6..5b6f5696 100644 --- a/benchmark/str.benchmark.catch2.cpp +++ b/benchmark/str.benchmark.catch2.cpp @@ -99,7 +99,7 @@ auto minimumUsedBytes(const STR &s) { // example, allocation size is 64, the mask is then 0b1.1111 (5 bits) // and size is 17: that's 0b1.0001, the bytes 18 to 63 are not used, // so, the bytes that this code takes are 63 - return size | roundSizeToAllocationSizeMask; + return (size | roundSizeToAllocationSizeMask) + sizeof(STR); } using ZStr = zoo::Str; From ad5c4fda7c625d9598a5d77afccd1f6664d65697 Mon Sep 17 00:00:00 2001 From: Eddie Date: Tue, 3 Dec 2024 17:08:02 -0800 Subject: [PATCH 11/16] Potential needed syntax fix in old infrastructure --- benchmark/str-experiment.src/Heap.cpp | 8 ++++++++ inc/zoo/AnyContainer.h | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 benchmark/str-experiment.src/Heap.cpp diff --git a/benchmark/str-experiment.src/Heap.cpp b/benchmark/str-experiment.src/Heap.cpp new file mode 100644 index 00000000..e7d113c6 --- /dev/null +++ b/benchmark/str-experiment.src/Heap.cpp @@ -0,0 +1,8 @@ +// +// Heap.cpp +// str-benchmark.catch2 +// +// Created by Eduardo Madrid on 12/3/24. +// + +#include diff --git a/inc/zoo/AnyContainer.h b/inc/zoo/AnyContainer.h index 55e80cf8..f7943ef3 100644 --- a/inc/zoo/AnyContainer.h +++ b/inc/zoo/AnyContainer.h @@ -298,7 +298,7 @@ struct MSVC_EMPTY_BASES AnyContainerBase: {} template - void emplaced(ValueType *ptr) noexcept { SuperContainer::template emplaced(ptr); } + void emplaced(ValueType *ptr) noexcept { SuperContainer::emplaced(ptr); } AnyContainerBase ©_assign(const AnyContainerBase &model) { SuperContainer::copy_assign(model); From dacbcd06f89c75791e6195d3ca753b2d94961811 Mon Sep 17 00:00:00 2001 From: Julio Date: Thu, 5 Dec 2024 17:49:16 -0800 Subject: [PATCH 12/16] Edit to the comment section --- benchmark/str-experiment/zoo/Str.h | 62 ++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h index b23caab3..cd1c17ae 100644 --- a/benchmark/str-experiment/zoo/Str.h +++ b/benchmark/str-experiment/zoo/Str.h @@ -31,13 +31,14 @@ inline void noOp(...) {} /// \brief Ah /// /// Design: -/// To minimize allocations, this type will attempt to hold the chars -/// of the string locally. +/// To minimize allocations, this type has a configurqble internal +/// buffer --of size N * sizeof(char *)-- that will hold the carac- +/// ters of a string short enough to fit in it. /// To maximize space efficiency, the encoding of whether the string /// is local or on the heap will occur at the last byte, least /// significant bits of the local storage. -/// the local storage doubles as the encoding of a heap pointer, for -/// strings that can not be held locally +/// The local storage doubles as the encoding of a heap pointer, for +/// strings that can not be held locally. /// This uses Andrei Alexandrescu's idea of storing the size of the /// string as the remaining local capacity, in this way, a 0 local /// capacity also encodes the null byte for c_str(), as reported by @@ -45,25 +46,52 @@ inline void noOp(...) {} /// In case the string is too long for local allocation, the /// pointer in the heap will be encoded locally in the last word /// of the representation. +/// Thus the optimal size of the local buffer is of sizeof(char *) +/// since it well either used as local storage or as pointer to the +/// heap. /// \tparam StorageModel Indicates the size of the local storage -/// and its alignment +/// and its alignment. /// For large strings (allocated in the heap): /// In the last void * worth of local storage: -/// +--------- Big Endian diagram ------------------------ -/// | Most significant part of Pointer | BytesForSizeEncoding -/// +--------- Little Endian actual representation +/// +---------------- Big Endian diagram -----------------------------+ +/// | Most significant part of Pointer | Last Byte, rob 4 lower bits | +/// +-----------------------------------------------------------------+ +/// Since we're robbing 4 bits from the last byte, this implies the +/// pointers are rounded to 16 bytes. /// For local strings: -/// Little Endian diagram -/// +----------------------------------------------------- -/// | characters... | remaining count -/// +------------------------------ +/// +------- Little Endian Diagam -----+ +/// | characters... | last byte | +/// +----------------------------------+ /// The last byte looks like this: /// For heap encoding: number of bytes of the size in the allocation, -/// the value is in the interval [1, 8], this is represented with 4 -/// bits in which the bit for -/// For local encoding there is the need for log2(Size) bits to express -/// the size of the local string (either as size or remaining) -/// And we need one more bit to encode local (zero) or heap(1) +/// the value is in the interval [1, 8], this is represented with a +/// shifted range of [0, 7] which allow us to use 3 bits to encode the +/// number of bytes used to encode the length of the string. A fourth +/// bit is used to flag whether the local buffer stores a pointer to a +/// heap allocated string (1) or if it is represented in the local bu= +/// ffer (0). In the heap encoding the binary number represented in such +/// bits, precedes the actual string payload. +/// For local encoding: the last bytes take the form of the null ter- +/// minator. +/// +/// The Case For A Custom Allocator. +/// A custom allocator was being considered for integration with the +/// StorageModel String. The considerations we see are: +/// * The size of the StorageModel can be set to size of char *, the +/// theoretical optimal size of the StorageModel String. +/// * The rest of the sizes can be divided in arenas (with increasing +/// limit sizes) to minimize the waste. We can assign some regular +/// (constexpr) step to these (say 1 between 8-15, 2 between 16-23, +/// and so on. that will depend on trial an error). +/// * An arena should be able to 'steal' a slot from an arena that has +/// a base size that is a multiple of the original. This will force +/// us to maintain metadata of the size and load factor for each +/// arena. +/// * A goal that the previous point tries to achieve is to minimize +/// the number of hits on the general allocator. +/// * The design has rely on touching only metadata. with just a visit +/// to the arena to mark the buffer as taken. +/// template struct Str { constexpr static auto From 2e27722b36b6b353e7129aa374975b5da2bbf3d3 Mon Sep 17 00:00:00 2001 From: thecppzoo Date: Thu, 5 Dec 2024 19:31:49 -0800 Subject: [PATCH 13/16] Update Str.h Improvements to comments --- benchmark/str-experiment/zoo/Str.h | 130 +++++++++++++++++------------ 1 file changed, 76 insertions(+), 54 deletions(-) diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h index cd1c17ae..0ed319ee 100644 --- a/benchmark/str-experiment/zoo/Str.h +++ b/benchmark/str-experiment/zoo/Str.h @@ -28,70 +28,92 @@ inline void noOp(...) {} #define printf noOp #define fprintf noOp -/// \brief Ah +/// \brief String controller type optimized for minimum +/// size and configurability /// /// Design: -/// To minimize allocations, this type has a configurqble internal -/// buffer --of size N * sizeof(char *)-- that will hold the carac- -/// ters of a string short enough to fit in it. -/// To maximize space efficiency, the encoding of whether the string -/// is local or on the heap will occur at the last byte, least -/// significant bits of the local storage. -/// The local storage doubles as the encoding of a heap pointer, for -/// strings that can not be held locally. -/// This uses Andrei Alexandrescu's idea of storing the size of the -/// string as the remaining local capacity, in this way, a 0 local -/// capacity also encodes the null byte for c_str(), as reported by -/// Nicholas Omrod at CPPCon 2016. -/// In case the string is too long for local allocation, the -/// pointer in the heap will be encoded locally in the last word -/// of the representation. -/// Thus the optimal size of the local buffer is of sizeof(char *) -/// since it well either used as local storage or as pointer to the -/// heap. -/// \tparam StorageModel Indicates the size of the local storage -/// and its alignment. +/// This type attempts to use a minimum size fully usable string type +/// compatible with the API of std::string. The extreme case is when +/// sizeof(Str) == sizeof(void *). This type is a template that +/// takes a type argument to serve as the *prototype* for the local +/// buffer. If you select "void *" as the prototype, the local buffer and +/// the size of Str will have size and alignment of void *. If you +/// select "void *[2]" (array of two pointers), then there will be 2 void +/// pointers worth of space (16 bytes) locally. +/// +/// To maximize space efficiency, the encoding of whether the string is +/// local or on the heap will occur at the last byte, least significant +/// bits of the local storage. The space corresponding to the last pointer +/// worth of local storage doubles as the encoding of a heap pointer for +/// strings that cannot be held locally. +/// +/// This uses Andrei Alexandrescu's idea of storing the size of the string +/// as the remaining local capacity. In this way, a 0 local capacity also +/// encodes the null byte for c_str(), as reported by Nicholas Omrod at +/// CPPCon 2016. In case the string is too long for local allocation, the +/// pointer in the heap will be encoded locally in the last word of the +/// representation. +/// +/// For an application expecting large strings, the optimal size of the +/// local storage would be that of a "char *". Since most strings would be +/// large, there would be few chances of storing them in the local buffer. +/// You could use "char *" or "void *" for the storage prototype. +/// +/// \tparam StorageModel Indicates the size of the local storage and its +/// alignment. +/// /// For large strings (allocated in the heap): /// In the last void * worth of local storage: /// +---------------- Big Endian diagram -----------------------------+ /// | Most significant part of Pointer | Last Byte, rob 4 lower bits | /// +-----------------------------------------------------------------+ -/// Since we're robbing 4 bits from the last byte, this implies the -/// pointers are rounded to 16 bytes. +/// Since we're robbing 4 bits from the last byte, this implies the pointers +/// are rounded up to 16 bytes. +/// /// For local strings: -/// +------- Little Endian Diagam -----+ -/// | characters... | last byte | -/// +----------------------------------+ +/// +------- Little Endian Diagram -----+ +/// | characters... | last byte | +/// +-----------------------------------+ +/// /// The last byte looks like this: -/// For heap encoding: number of bytes of the size in the allocation, -/// the value is in the interval [1, 8], this is represented with a -/// shifted range of [0, 7] which allow us to use 3 bits to encode the -/// number of bytes used to encode the length of the string. A fourth -/// bit is used to flag whether the local buffer stores a pointer to a -/// heap allocated string (1) or if it is represented in the local bu= -/// ffer (0). In the heap encoding the binary number represented in such -/// bits, precedes the actual string payload. -/// For local encoding: the last bytes take the form of the null ter- -/// minator. +/// For heap encoding: the number of bytes required to encode the size of +/// the string, or mathematically, the logarithm base 256 of the string's +/// size. This value will be in the interval [1, 8], encoded with a shifted +/// range of [0, 7], allowing us to use 3 bits to encode the length. An +/// extra bit flags whether the local buffer stores a pointer to a heap +/// string (1) or if it is represented in the local buffer (0). +/// +/// Because the storage model can be of arbitrary size, we cannot assume +/// the lowest available bit will encode 8. It will be the lowest bit the +/// local buffer size allows. For a storage model of void *[4], the heap +/// flag encodes for 32 as a number. For Str, the bit encodes for 8. +/// This strategy forces allocations to align to the ceiling of +/// log2(sizeof(StorageModel)). +/// +/// Local strings: +/// The last byte serves as the COUNT OF REMAINING BYTES, including the null +/// character terminator. Hence, Str can hold up to seven bytes +/// + null terminator locally. Str allows 8*8 = 64 characters +/// locally (including the null). +/// +/// Heap-allocated strings: +/// As explained, the lower three bits indicate the number of bytes needed +/// to encode the string's length, shifted by 1. The memory referenced by +/// the encoded pointer starts with however many bytes are needed to encode +/// the length, followed by the actual bytes of the string. /// -/// The Case For A Custom Allocator. -/// A custom allocator was being considered for integration with the -/// StorageModel String. The considerations we see are: -/// * The size of the StorageModel can be set to size of char *, the -/// theoretical optimal size of the StorageModel String. -/// * The rest of the sizes can be divided in arenas (with increasing -/// limit sizes) to minimize the waste. We can assign some regular -/// (constexpr) step to these (say 1 between 8-15, 2 between 16-23, -/// and so on. that will depend on trial an error). -/// * An arena should be able to 'steal' a slot from an arena that has -/// a base size that is a multiple of the original. This will force -/// us to maintain metadata of the size and load factor for each -/// arena. -/// * A goal that the previous point tries to achieve is to minimize -/// the number of hits on the general allocator. -/// * The design has rely on touching only metadata. with just a visit -/// to the arena to mark the buffer as taken. -/// +/// The Case For A Custom Allocator: +/// A custom allocator is considered for integration with the StorageModel +/// for Str. The considerations are: +/// * The size of the StorageModel can be set to the size of char *, the +/// theoretical optimal size of the StorageModel. +/// * The remaining sizes can be divided into arenas (with increasing limit +/// sizes) to minimize waste. Regular (constexpr) steps (e.g., 1 between +/// 8-15, 2 between 16-23, etc.) can be trialed. +/// * An arena should 'steal' a slot from an arena whose base size is a +/// multiple of the original. This forces us to maintain metadata on the +/// size and load factor for each arena. +/// * The previous point aims to minimize hits on the general allocator. template struct Str { constexpr static auto From aca36ba0ced2199c6427e2f784f9879f700c0b4b Mon Sep 17 00:00:00 2001 From: Julio Date: Sat, 7 Dec 2024 16:30:22 -0800 Subject: [PATCH 14/16] lastByte in a constant context should not return a reference --- benchmark/str-experiment/zoo/Str.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h index 0ed319ee..f9817ab0 100644 --- a/benchmark/str-experiment/zoo/Str.h +++ b/benchmark/str-experiment/zoo/Str.h @@ -126,7 +126,7 @@ struct Str { static_assert(sizeof(void *) <= Size); alignas(alignof(StorageModel)) char buffer_[sizeof(StorageModel)]; - auto &lastByte() const noexcept { return buffer_[Size - 1]; } + auto lastByte() const noexcept { return buffer_[Size - 1]; } auto &lastByte() noexcept { return buffer_[Size - 1]; } auto lastPtr() const noexcept { return buffer_ + Size - sizeof(char *); } auto lastPtr() noexcept { return buffer_ + Size - sizeof(char *); } From 0e03aeb2d08c839698ace6f8d8014035239a966d Mon Sep 17 00:00:00 2001 From: Julio Date: Sat, 7 Dec 2024 16:45:50 -0800 Subject: [PATCH 15/16] Renaming lastPtr() to codePtr(), to better reflect its purpose --- benchmark/str-experiment/zoo/Str.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h index f9817ab0..466da137 100644 --- a/benchmark/str-experiment/zoo/Str.h +++ b/benchmark/str-experiment/zoo/Str.h @@ -128,11 +128,11 @@ struct Str { auto lastByte() const noexcept { return buffer_[Size - 1]; } auto &lastByte() noexcept { return buffer_[Size - 1]; } - auto lastPtr() const noexcept { return buffer_ + Size - sizeof(char *); } - auto lastPtr() noexcept { return buffer_ + Size - sizeof(char *); } + auto codePtr() const noexcept { return buffer_ + Size - sizeof(char *); } + auto codePtr() noexcept { return buffer_ + Size - sizeof(char *); } auto allocationPtr() const noexcept { uintptr_t codeThatContainsAPointer; - memcpy(&codeThatContainsAPointer, lastPtr(), sizeof(char *)); + memcpy(&codeThatContainsAPointer, codePtr(), sizeof(char *)); assert(onHeap()); auto asLittleEndian = __builtin_bswap64(codeThatContainsAPointer); auto bytesForSizeMinus1 = asLittleEndian & 7; @@ -189,7 +189,7 @@ struct Str { memcpy(onHeap + bytesForSize, source, length); auto code = encodePointer(bytesForSize, onHeap); - auto addressOfLastPointerWorthOfSpace = lastPtr(); + auto addressOfLastPointerWorthOfSpace = codePtr(); memcpy(addressOfLastPointerWorthOfSpace, &code, sizeof(char *)); // We encode the range [1..8] as [0..7] From bfb83be68bba1ece1da0e85f3c4a3c74ed96aa98 Mon Sep 17 00:00:00 2001 From: thecppzoo Date: Sun, 8 Dec 2024 12:26:50 -0800 Subject: [PATCH 16/16] codePtr ought to return pointer to pointer --- benchmark/str-experiment/zoo/Str.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h index 466da137..81075c74 100644 --- a/benchmark/str-experiment/zoo/Str.h +++ b/benchmark/str-experiment/zoo/Str.h @@ -128,8 +128,9 @@ struct Str { auto lastByte() const noexcept { return buffer_[Size - 1]; } auto &lastByte() noexcept { return buffer_[Size - 1]; } - auto codePtr() const noexcept { return buffer_ + Size - sizeof(char *); } - auto codePtr() noexcept { return buffer_ + Size - sizeof(char *); } + auto codePtr() noexcept { return reinterpret_cast(buffer_ + Size - sizeof(char *)); } + const auto codePtr() const noexcept { return const_cast(this)->codePtr(); } + auto allocationPtr() const noexcept { uintptr_t codeThatContainsAPointer; memcpy(&codeThatContainsAPointer, codePtr(), sizeof(char *));