Skip to content

Change RedisConnection::Query::value_type from String to std::variant<const char*,String> #10391

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

Al2Klimov
Copy link
Member

(Find TL;DR below)

Seriously speaking, 70ca88a pollutes the diff and should be viewed on its own. It just replaces std::vector<String> with RedisConnection::Query. As RedisConnection::Query already was a std::vector<String>, this is a no-op on its own. But it allows to just change RedisConnection::Query itself and affect all its usages at once.

Why do we have to change String to std::variant<const char*,String> at all? (efe5f22)

Especially our history messages contain lots of hardcoded C string literals "like this one". At runtime, they get translated to pointers to constant global memory, const char*. String malloc(3)s and copies these data every time. In contrast, std::variant<const char*,String> just stores the address if any.

TL;DR

  • The JSON RPC message processing involves Icinga DB handlers
  • The Icinga DB handlers assemble Redis messages, std::vector<T>
  • The faster T is, the better for JSON RPC message processing
  • The fastest possible T is const char*, if any, which is just a pointer

fixes #10276
ref/NC/820479

@Al2Klimov Al2Klimov added area/icingadb New backend core/quality Improve code, libraries, algorithms, inline docs ref/NC labels Mar 25, 2025
@Al2Klimov Al2Klimov requested a review from lippserd March 25, 2025 16:58
@cla-bot cla-bot bot added the cla/signed label Mar 25, 2025
@Al2Klimov
Copy link
Member Author

Strange, it worked on my machine...

@Al2Klimov Al2Klimov marked this pull request as draft March 25, 2025 17:07
@Al2Klimov Al2Klimov removed the request for review from lippserd March 25, 2025 17:07
@julianbrost
Copy link
Contributor

The idea looks fine in general. Besides getting it to work, you might want to take a look at std::string_view. If you implement a conversion to it once, that should allow to avoid other case-distinctions (as the purpose of string_view is to abstract how exactly the backing string is stored).

@Al2Klimov
Copy link
Member Author

Like, class TODO : public std::variant<const char*,String> { operator std::string_view(); };?

@julianbrost
Copy link
Contributor

Probably something like that. A function for it might work too, but a small class could be nicer overall.

@Al2Klimov Al2Klimov force-pushed the redis-query-variant branch from ab1d887 to 7a5d6f3 Compare March 31, 2025 10:53
@Al2Klimov Al2Klimov marked this pull request as ready for review March 31, 2025 10:53
Comment on lines 405 to 394
auto variantToString = [](RedisConnection::QueryArg v) -> String {
if (auto str (std::get_if<String>(&v.GetData())); str) {
return std::move(*str);
}

auto sv (std::get<std::string_view>(v.GetData()));

return {sv.data(), sv.data() + sv.size()};
};

dest.emplace(variantToString(std::move(hMSet[i])), variantToString(std::move(hMSet[i + 1u])));
Copy link
Member

Choose a reason for hiding this comment

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

Same here as well! If there was a corresponding constructor of the String class that takes std::string_view as an argument, you could just use dest.emplace(std::string_view(hMSet[i]), ...) instead. I've played around a bit with this:

No, really, moving a return value is easy
#include <iostream>
#include <variant>
#include <vector>

struct String {
    std::string data;

    String(): data("Orig") {}
    String(std::string_view sv): data(sv.data(), sv.size()) {}
    String(const String& o): data(o.data+" - Copy") {}
    String(String&& o) noexcept : data(o.data+" - Moved") {}

    String operator=(const String& o) {
        return String(o);
    }

    String operator=(String&& o) noexcept {
        return String(o);
    }
    
    operator std::string_view() const {
        return {data.c_str(), data.size()};
    }
};

struct Variant {
    std::variant<std::string_view, String> Data;

    Variant(std::string_view data): Data(data) {}
    Variant(String data): Data(std::move(data)) {}
    Variant(const char* data): Data(std::string_view(data)) {}

    operator std::string_view() const {
        if (std::holds_alternative<String>(Data)) {
            return std::get<String>(Data);
        }

        return std::get<std::string_view>(Data);
    }
};

int main()
{
    std::vector<Variant> variants{Variant("Const Char"), Variant(String{}), Variant(String{}), Variant(String{})};

    for(auto vv : variants)
        printf("%s\n", std::string_view(vv).data());

    std::cout << "\nStart transforming variants to String()\n";

    std::vector<String> strings;
    for(auto& v : variants) {
        strings.emplace_back(std::string_view(v));
    }

    for(auto& str : strings) {
        printf("%s\n", str.data.c_str());
    }
    std::cout << "End transforming variants to String()\n\n";
    
    for(auto vv : variants)
        std::cout << std::string_view(vv) << std::endl;
}
Const Char
Orig - Moved - Copy - Copy
Orig - Moved - Copy - Copy
Orig - Moved - Copy - Copy

Start transforming variants to String()
Const Char - Moved - Moved
Orig - Moved - Copy - Moved
Orig - Moved - Copy
Orig - Moved - Copy
End transforming variants to String()

Const Char
Orig - Moved - Copy - Copy
Orig - Moved - Copy - Copy
Orig - Moved - Copy - Copy

Copy link
Member Author

Choose a reason for hiding this comment

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

How does std::string_view conversion move the original value?

Copy link
Member

Choose a reason for hiding this comment

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

How does std::string_view conversion move the original value?

It doesn't but why do you still need that conversion at all? We already discussed about this in person but you still didn't changed it, so why? std::variant<...> already provides a proper implementation of comparison operators and you just have to introduce those operator implementations for the new QueryArg class and forward the request to the underlying std::veriant<...> implementation:

bool operator==(const QueryArg& other) const;
bool operator!=(const QueryArg& other) const;
bool operator<(const QueryArg& other) const;
bool operator>(const QueryArg& other) const;

You can then use QueryArg everywhere if it's needed, YES, even as a std::map<> key.

Copy link
Member Author

Choose a reason for hiding this comment

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

How does std::string_view conversion move the original value?

It doesn't but why do you still need that conversion at all?

For the statically typed code to compile.

🤷‍♂️

We already discussed about this in person but you still didn't changed it, so why?

Yes, we already discussed in person that you want me to go down this rabbit hole which will improve literally nothing:

dest is part of ourContent which is divided into ourCheckSums and ourObjects. I.e dest is a mapping from a computed id to some computed fancy stuff. Those runtime-assembled Strings haven't even nearly anything to do with "hardcoded C string literals" which I'm handling here.

you just have to introduce those operator implementations for the new QueryArg class and forward the request to the underlying std::veriant<...> implementation:

bool operator==(const QueryArg& other) const;
bool operator!=(const QueryArg& other) const;
bool operator<(const QueryArg& other) const;
bool operator>(const QueryArg& other) const;

These are at least four, for my implementation one function is enough.

You can then use QueryArg everywhere if it's needed, YES, even as a std::map<> key.

But what exactly for?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, we already discussed in person that you want me to go down this rabbit hole which will improve literally nothing:

I have absolutely no idea what you're talking about. Which rabbit hole are you referring to? It's just a simple type change, like you did for the other parts. Did you even try it?

I.e dest is a mapping from a computed id to some computed fancy stuff. Those runtime-assembled Strings haven't even nearly anything to do with "hardcoded C string literals" which I'm handling here.

You are literally undoing thousands of objects serialised using QueryArg, simply because you don't want to change the type of ourContent local variable. This PR is about making Icinga DB more efficient, so I don't know why we're still even talking about it. If you're not willing to provide a full implementation, then why open a PR? As I have already pointed out, it's a fairly simple change and doesn't require any tweaking, but for some reason you don't even want to bother trying.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we already discussed in person that you want me to go down this rabbit hole which will improve literally nothing:

I have absolutely no idea what you're talking about. Which rabbit hole are you referring to? It's just a simple type change, like you did for the other parts. Did you even try it?

This by itself is indeed a simple type change:

--- a/lib/icingadb/icingadb-objects.cpp
+++ b/lib/icingadb/icingadb-objects.cpp
@@ -378 +378 @@ void IcingaDB::UpdateAllConfigObjects()
-               std::map<String, std::map<String, String>> ourContent;
+               std::map<String, std::map<RedisConnection::QueryArg, RedisConnection::QueryArg>> ourContent;
@@ -394 +394 @@ void IcingaDB::UpdateAllConfigObjects()
-                                               dest.emplace(variantToString(std::move(hMSet[i])), variantToString(std::move(hMSet[i + 1u])));
+                                               dest.emplace(std::move(hMSet[i]), std::move(hMSet[i + 1u]));

But this won't compile, so we'd need some operators, as you already mentioned. My implementation just needs one function.

I.e dest is a mapping from a computed id to some computed fancy stuff. Those runtime-assembled Strings haven't even nearly anything to do with "hardcoded C string literals" which I'm handling here.

You are literally undoing thousands of objects serialised using QueryArg, simply because you don't want to change the type of ourContent local variable.

Absolutely not! QueryArg is simply about distinguishing two things:

  • pointers to C strings which are hardcoded in global memory (borrowed)
  • C++/Icinga strings which are computed at runtime and malloc(3)ed (owned)

For the owned it doesn't matter at all whether it's a plain String or wrapped in a QueryArg. For the borrowed one it matters of course!

But!

Show me one QueryArg I pass to variantToString() which is borrowed, not owned. Just one!

This PR is about making Icinga DB more efficient, so I don't know why we're still even talking about it.

And, given what I said above, where exactly the efficiency gain?

If you're not willing to provide a full implementation, then why open a PR?

It is a full implementation according to the scope being efficiency.

Comment on lines +75 to +84
std::variant<std::string_view, String>& GetData()
{
return m_Data;
}
Copy link
Member

Choose a reason for hiding this comment

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

With the above suggestions applied, this should become obsolete.

@Al2Klimov Al2Klimov force-pushed the redis-query-variant branch from 7a5d6f3 to 050665e Compare April 2, 2025 11:02
…d::vector)

This expresses what kind of vector it is and allows to easily change those types in the future.
…<const char*,String>

Especially our history messages contain lots of hardcoded C string literals "like this one". At runtime, they get translated to pointers to constant global memory, const char*. String malloc(3)s and copies these data every time. In contrast, std::variant<const char*,String> just stores the address if any. (Actually, const char* is wrapped by std::string_view to not compute its length every time.)
@Al2Klimov Al2Klimov force-pushed the redis-query-variant branch from 050665e to cfc5532 Compare April 7, 2025 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/icingadb New backend cla/signed core/quality Improve code, libraries, algorithms, inline docs ref/NC
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Icinga DB Boost.Signal2 handlers: reduce malloc(3)
3 participants