Skip to content

Commit

Permalink
Optimize storage of tiny strings (up to 3 characters)
Browse files Browse the repository at this point in the history
  • Loading branch information
bblanchon committed Feb 27, 2025
1 parent cb1dcfa commit 1eddeb0
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 6 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ HEAD
----

* Fix conversion from static string to number
* Slightly reduce code size
* Optimize storage of tiny strings (up to 3 characters)

v7.3.0 (2024-12-29)
------
Expand Down
6 changes: 3 additions & 3 deletions extras/tests/JsonObject/set.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ TEST_CASE("JsonObject::set()") {
JsonDocument doc3(&timebomb);
JsonObject obj3 = doc3.to<JsonObject>();

obj1["a"_s] = 1;
obj1["b"_s] = 2;
obj1["alpha"_s] = 1;
obj1["beta"_s] = 2;

bool success = obj3.set(obj1);

REQUIRE(success == false);
REQUIRE(doc3.as<std::string>() == "{\"a\":1}");
REQUIRE(doc3.as<std::string>() == "{\"alpha\":1}");
}

SECTION("copy fails in the middle of an array") {
Expand Down
14 changes: 13 additions & 1 deletion extras/tests/JsonVariant/as.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ TEST_CASE("JsonVariant::as()") {
REQUIRE(variant.as<JsonString>() == "hello");
}

SECTION("set(std::string(\"4.2\"))") {
SECTION("set(std::string(\"4.2\")) (tiny string optimization)") {
variant.set("4.2"_s);

REQUIRE(variant.as<bool>() == true);
Expand All @@ -211,6 +211,18 @@ TEST_CASE("JsonVariant::as()") {
REQUIRE(variant.as<JsonString>().isStatic() == false);
}

SECTION("set(std::string(\"123.45\"))") {
variant.set("123.45"_s);

REQUIRE(variant.as<bool>() == true);
REQUIRE(variant.as<long>() == 123L);
REQUIRE(variant.as<double>() == Approx(123.45));
REQUIRE(variant.as<const char*>() == "123.45"_s);
REQUIRE(variant.as<std::string>() == "123.45"_s);
REQUIRE(variant.as<JsonString>() == "123.45");
REQUIRE(variant.as<JsonString>().isStatic() == false);
}

SECTION("set(\"true\")") {
variant.set("true");

Expand Down
12 changes: 12 additions & 0 deletions extras/tests/JsonVariant/set.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ TEST_CASE("JsonVariant::set() when there is enough memory") {
});
}

SECTION("char* (tiny string optimization)") {
char str[16];

strcpy(str, "abc");
bool result = variant.set(str);
strcpy(str, "def");

REQUIRE(result == true);
REQUIRE(variant == "abc"); // stores by copy
REQUIRE(spy.log() == AllocatorLog{});
}

SECTION("(char*)0") {
bool result = variant.set(static_cast<char*>(0));

Expand Down
4 changes: 4 additions & 0 deletions src/ArduinoJson/Variant/VariantContent.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum class VariantTypeBits : uint8_t {

enum class VariantType : uint8_t {
Null = 0, // 0000 0000
TinyString = 0x02, // 0000 0010
RawString = 0x03, // 0000 0011
LinkedString = 0x04, // 0000 0100
OwnedString = 0x05, // 0000 0101
Expand All @@ -46,6 +47,8 @@ inline bool operator&(VariantType type, VariantTypeBits bit) {
return (uint8_t(type) & uint8_t(bit)) != 0;
}

const size_t tinyStringMaxLength = 3;

union VariantContent {
VariantContent() {}

Expand All @@ -61,6 +64,7 @@ union VariantContent {
CollectionData asCollection;
const char* asLinkedString;
struct StringNode* asOwnedString;
char asTinyString[tinyStringMaxLength + 1];
};

#if ARDUINOJSON_USE_EXTENSIONS
Expand Down
22 changes: 21 additions & 1 deletion src/ArduinoJson/Variant/VariantData.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class VariantData {
case VariantType::Object:
return visit.visit(content_.asObject);

case VariantType::TinyString:
return visit.visit(JsonString(content_.asTinyString));

case VariantType::LinkedString:
return visit.visit(JsonString(content_.asLinkedString, true));

Expand Down Expand Up @@ -199,6 +202,9 @@ class VariantData {
case VariantType::Int64:
return static_cast<T>(extension->asInt64);
#endif
case VariantType::TinyString:
str = content_.asTinyString;
break;
case VariantType::LinkedString:
str = content_.asLinkedString;
break;
Expand Down Expand Up @@ -241,6 +247,9 @@ class VariantData {
case VariantType::Int64:
return convertNumber<T>(extension->asInt64);
#endif
case VariantType::TinyString:
str = content_.asTinyString;
break;
case VariantType::LinkedString:
str = content_.asLinkedString;
break;
Expand Down Expand Up @@ -281,6 +290,8 @@ class VariantData {

JsonString asString() const {
switch (type_) {
case VariantType::TinyString:
return JsonString(content_.asTinyString);
case VariantType::LinkedString:
return JsonString(content_.asLinkedString, true);
case VariantType::OwnedString:
Expand Down Expand Up @@ -395,7 +406,8 @@ class VariantData {

bool isString() const {
return type_ == VariantType::LinkedString ||
type_ == VariantType::OwnedString;
type_ == VariantType::OwnedString ||
type_ == VariantType::TinyString;
}

size_t nesting(const ResourceManager* resources) const {
Expand Down Expand Up @@ -509,6 +521,14 @@ class VariantData {
content_.asLinkedString = s;
}

void setTinyString(const char* s, uint8_t n) {
ARDUINOJSON_ASSERT(type_ == VariantType::Null); // must call clear() first
ARDUINOJSON_ASSERT(s);
type_ = VariantType::TinyString;
for (uint8_t i = 0; i < n + 1; i++)
content_.asTinyString[i] = s[i];
}

void setOwnedString(StringNode* s) {
ARDUINOJSON_ASSERT(type_ == VariantType::Null); // must call clear() first
ARDUINOJSON_ASSERT(s);
Expand Down
12 changes: 12 additions & 0 deletions src/ArduinoJson/Variant/VariantImpl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ inline bool VariantData::setString(TAdaptedString value,
return true;
}

if (value.size() <= tinyStringMaxLength) {
uint8_t length = static_cast<uint8_t>(value.size());
bool containsNul = false;
for (uint8_t i = 0; i < length; i++)
containsNul |= !value[i];
bool nulTerminated = value[length] == 0;
if (!containsNul && nulTerminated) {
setTinyString(value.data(), length);
return true;
}
}

auto dup = resources->saveString(value);
if (dup) {
setOwnedString(dup);
Expand Down

0 comments on commit 1eddeb0

Please sign in to comment.