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

Cleaner, faster StringBuilder implementation #77152

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 28 additions & 46 deletions core/string/string_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,63 +37,45 @@ StringBuilder &StringBuilder::append(const String &p_string) {
return *this;
}

strings.push_back(p_string);
appended_strings.push_back(-1);
uint32_t len = p_string.length();

string_length += p_string.length();

return *this;
}

StringBuilder &StringBuilder::append(const char *p_cstring) {
int32_t len = strlen(p_cstring);
uint32_t total_length = string_length + len;
if (capacity < total_length) {
while (capacity < total_length) {
capacity <<= 1;
}
buffer = (char32_t *)memrealloc(buffer, capacity * sizeof(char32_t));
}

c_strings.push_back(p_cstring);
appended_strings.push_back(len);
memcpy(buffer + string_length, p_string.ptr(), len * sizeof(char32_t));

string_length += len;
string_length = total_length;
strings++;

return *this;
}

String StringBuilder::as_string() const {
if (string_length == 0) {
return "";
}

char32_t *buffer = memnew_arr(char32_t, string_length);

int current_position = 0;

int godot_string_elem = 0;
int c_string_elem = 0;

for (int i = 0; i < appended_strings.size(); i++) {
if (appended_strings[i] == -1) {
// Godot string
const String &s = strings[godot_string_elem];

memcpy(buffer + current_position, s.ptr(), s.length() * sizeof(char32_t));

current_position += s.length();

godot_string_elem++;
} else {
const char *s = c_strings[c_string_elem];

for (int32_t j = 0; j < appended_strings[i]; j++) {
buffer[current_position + j] = s[j];
}

current_position += appended_strings[i];
StringBuilder &StringBuilder::append(const char *p_cstring) {
uint32_t len = strlen(p_cstring);

c_string_elem++;
uint32_t total_length = string_length + len;
if (capacity < total_length) {
while (capacity < total_length) {
capacity <<= 1;
}
buffer = (char32_t *)memrealloc(buffer, capacity * sizeof(char32_t));
}

for (uint32_t i = 0; i < len; i++) {
buffer[string_length + i] = p_cstring[i];
}

String final_string = String(buffer, string_length);
string_length = total_length;
strings++;

memdelete_arr(buffer);
return *this;
}

return final_string;
String StringBuilder::as_string() const {
return String(buffer, string_length);
}
19 changes: 11 additions & 8 deletions core/string/string_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,10 @@

class StringBuilder {
uint32_t string_length = 0;
uint32_t capacity = 16;
uint32_t strings = 0;

Vector<String> strings;
Vector<const char *> c_strings;

// -1 means it's a Godot String
// a natural number means C string.
Vector<int32_t> appended_strings;
char32_t *buffer;

Copy link
Member

Choose a reason for hiding this comment

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

Maybe you can just use a LocalVector instead of handling memory internally? This whole class seems much like a wrapper on LocalVector. 🤔

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 would actually just increase the code size, and force me to double-check that every assumption holds for the specific method calls I use. And would be susceptible to future changes in LocalVector. I refer you to the story of how left-pad took down the internet. Not everything has to be a web of inter-dependencies.

At any rate the discussion is moot. It turns out there's another class called StringBuffer that does everything this naive implementation does, plus a couple of extras, achieving better performance on benchmarks. So an even smaller solution would be to just typedef StringBuilder to StringBuffer. Or remove use of the prior from the code completely.

public:
StringBuilder &append(const String &p_string);
Expand All @@ -65,7 +62,7 @@ class StringBuilder {
}

_FORCE_INLINE_ int num_strings_appended() const {
return appended_strings.size();
return strings;
}

_FORCE_INLINE_ uint32_t get_string_length() const {
Expand All @@ -78,7 +75,13 @@ class StringBuilder {
return as_string();
}

StringBuilder() {}
StringBuilder() {
buffer = (char32_t *)memalloc(16 * sizeof(char32_t));
}

~StringBuilder() {
memfree(buffer);
}
};

#endif // STRING_BUILDER_H