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

short string optimization #131

Merged

Conversation

Kosta-Github
Copy link
Contributor

Since the payload (the Data union) of the current implementation of GenericValue is 12 bytes (32 bit mode) or 16 bytes (64 bit mode) it could store UTF8-encoded strings up to 11 or 15 chars plus the terminating zero character plus the actual string length.

    struct ShortString {
        enum { MaxChars = sizeof(String) / sizeof(Ch), MaxSize = MaxChars - 1, LenPos = MaxSize };
        Ch str[MaxChars];

        inline static bool Usable(SizeType len) { return            (MaxSize >= len); }
        inline void     SetLength(SizeType len) { str[LenPos] = (Ch)(MaxSize -  len); }
        inline SizeType GetLength() const       { return  (SizeType)(MaxSize -  str[LenPos]); }
    };  // at most as many bytes as "String" above => 12 bytes in 32-bit mode, 16 bytes in 64-bit mode

The ShortString can represent zero-terminated strings up to MaxSize chars (excluding the terminating zero) and store a value to determine the length of the contained string in the last character str[LenPos] by storing MaxSize - length there. If the string to store has the maximal length of MaxSize (excluding the terminating zero) then str[LenPos] will store 0 and therefore act as the string terminator as well. For getting the string length back from that value just use MaxSize - str[LenPos].

This allows to store 11-chars strings in 32-bit mode and 15-chars strings in 64-bit mode inline (for UTF8-encoded strings).

The integration into GenericValue is done by introducing additional kInlineStrFlag and kShortStringFlag flags. When setting a new string value in SetStringRaw(s, alloc) it is first checked if the string is short enough to fit into the inline string buffer and if so the given source string will be copied into the new ShortString target instead of allocating additional memory for it.

Since the payload (the `Data` union) of the current implementation of `GenericValue` is `12 bytes` (32 bit) or `16 bytes` (64 bit) it could store `UTF8`-encoded strings up to `10` or `14` chars plus the `terminating zero` character plus the string length:
``` C++
    struct ShortString {
        enum { MaxSize = sizeof(GenericValue::String) / sizeof(Ch) - sizeof(unsigned char) };
        Ch str[MaxSize];
        unsigned char length;
    };  // at most as many bytes as "String" above => 12 bytes in 32-bit mode, 16 bytes in 64-bit mode

```

This is achieved by introducing additional `kInlineStrFlag` and `kShortStringFlag` flags. When setting a new string value in `SetStringRaw(s, alloc)` it is first checked if the string is short enough to fit into the `inline string buffer` and if so the given source string will be copied into the new `ShortString` target instead of allocating additional memory for it.
Instead of replicating the functionality of `GetString()` and `GetStringLength()` in `StringEqual()` it now calls these methods instead.
The `ShortString` can represent zero-terminated strings up to `MaxSize` chars (excluding the terminating zero) and store a value to determine the length of the contained string in the last character `str[LenPos]` by storing `MaxSize - length` there. If the string to store has the maximal length of `MaxSize` (excluding the terminating zero) then `str[LenPos]` will store `0` and therefore act as the string terminator as well. For getting the string length back from that value just use `MaxSize - str[LenPos]`.

This allows to store `11`-chars strings in 32-bit mode and `15`-chars strings in 64-bit mode inline (for `UTF8`-encoded strings).
@Kosta-Github
Copy link
Contributor Author

And by reworking the GenericValue::flags_ thing a bit to only eat up only 1 byte (instead of 1 int) you could end up with storing 15-chars (32-bit mode) and 19-chars (64-bit mode) inline...

@@ -1084,3 +1084,26 @@ TEST(Document, CrtAllocator) {
V a(kArrayType);
a.PushBack(1, allocator); // Should not call destructor on uninitialized Value of newly allocated elements.
}

static void TestShortStringOptimization(const char* str) {
const int len = (int)strlen(str);
Copy link
Collaborator

Choose a reason for hiding this comment

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

len is unused?

} else {
flags_ = kCopyStringFlag;
data_.s.length = s.length;
str = (Ch *)allocator.Malloc((s.length + 1) * sizeof(Ch));
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems necessary to handle short string in ~GenericValue(), as copy-string is malloc here but short-string does not need to. Shall generate crash in unit test when using CrtAllocator.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, in line 1488 I use the new flag kShortStringFlag and the dtor checks for kCopyStringFlag...

Copy link
Collaborator

Choose a reason for hiding this comment

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

You are right.

@Kosta-Github
Copy link
Contributor Author

sorry for the failing unit tests spam; I haven't setup gtest on my Windows system, so I was relying on Travis as a compiler for the unit tests...

@pah
Copy link
Contributor

pah commented Sep 1, 2014

LGTM 👍 .

It would be good to explicitly test the implementation for other encodings, especially those with bigger character types (UTF32<>). And it might be useful to benchmark the effects, especially when used in combination with the default MemoryPoolAllocator. At the end, we may want to have some configuration switch (template parameter, traits, ...) to enable/disable this feature in different application scenarios.

@drewnoakes
Copy link
Contributor

If the string to store has the maximal length of MaxSize (excluding the terminating zero) then str[LenPos] will store 0 and therefore act as the string terminator as well.

Is it then possible to represent a zero-length string and still compute a correct length without checking if str[0]==0?

@Kosta-Github
Copy link
Contributor Author

@drewnoakes yes, a zero-length string stores 0x00 in str[0] in order to provide a zero-terminated string at str, and 15 in str[LenPos] and MaxSize is also 15 in case of 64-bit mode and therefore MaxSize - str[LenPos] will become 15 - 15 will become 0 for the string length...

@drewnoakes
Copy link
Contributor

@Kosta-Github -- that's a neat trick which I didn't grok on the first read. Nice. So the 'length' can really be thought of as the padding length, rather than the string length.

@pah
Copy link
Contributor

pah commented Sep 2, 2014

I tried the nativejson-benchmark on a 32-bit Linux machine (using Clang 3.6) and obtained the following preliminary results:

- HEAD ShortStringOpt
Parse 25ms, 5.1MB (103 allocations) 21ms 4.7MB (97 allocations)
Stringify 31ms 32ms
Prettify 45ms 39ms

So it seems to be a small but promising optimization for RapidJSON. When using the CrtAllocator, the benefit should be higher, of course.

miloyip added a commit that referenced this pull request Sep 2, 2014
@miloyip miloyip merged commit 9289f32 into Tencent:master Sep 2, 2014
@Kosta-Github Kosta-Github deleted the Kosta/short_string_optimization branch September 2, 2014 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants