[channel_args] Size optimizations (#33901)

- Make `Value` a simple wrapper around `Pointer` and use some blessed
vtables to distinguish strings vs ints vs actual pointers - this saves 8
bytes per value stored
- introduce `RcString` as a lightweight container around an immutable
string - this saves some bytes vs the shared_ptr<std::string> approach
we previously had, and importantly opens up the technique (via
`RcStringValue`) to channel node keys also, which should increase
sharing and consequently also decrease total memory usage

---------

Co-authored-by: ctiller <ctiller@users.noreply.github.com>
pull/33919/head
Craig Tiller 1 year ago committed by GitHub
parent 82e506c7b2
commit 8004254a53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      build_autogenerated.yaml
  2. 2
      src/core/BUILD
  3. 152
      src/core/lib/channel/channel_args.cc
  4. 130
      src/core/lib/channel/channel_args.h
  5. 11
      src/core/lib/compression/compression_internal.cc
  6. 4
      src/core/lib/gprpp/ref_counted.h

@ -7076,9 +7076,7 @@ targets:
- src/core/lib/event_engine/channel_args_endpoint_config.h
- src/core/lib/gprpp/atomic_utils.h
- src/core/lib/gprpp/dual_ref_counted.h
- src/core/lib/gprpp/match.h
- src/core/lib/gprpp/orphanable.h
- src/core/lib/gprpp/overload.h
- src/core/lib/gprpp/ref_counted.h
- src/core/lib/gprpp/ref_counted_ptr.h
- src/core/lib/gprpp/time.h

@ -2624,7 +2624,6 @@ grpc_cc_library(
"absl/strings",
"absl/strings:str_format",
"absl/types:optional",
"absl/types:variant",
],
language = "c++",
visibility = [
@ -2634,7 +2633,6 @@ grpc_cc_library(
"avl",
"channel_stack_type",
"dual_ref_counted",
"match",
"ref_counted",
"time",
"useful",

@ -27,6 +27,9 @@
#include <algorithm>
#include <initializer_list>
#include <map>
#include <memory>
#include <new>
#include <string>
#include <vector>
#include "absl/strings/match.h"
@ -40,11 +43,47 @@
#include <grpc/support/string_util.h>
#include "src/core/lib/gpr/useful.h"
#include "src/core/lib/gprpp/crash.h"
#include "src/core/lib/gprpp/match.h"
namespace grpc_core {
RefCountedPtr<RcString> RcString::Make(absl::string_view src) {
void* p = gpr_malloc(sizeof(Header) + src.length() + 1);
return RefCountedPtr<RcString>(new (p) RcString(src));
}
RcString::RcString(absl::string_view src) : header_{{}, src.length()} {
memcpy(payload_, src.data(), header_.length);
// Null terminate because we frequently need to convert to char* still to go
// back and forth to the old c-style api.
payload_[header_.length] = 0;
}
void RcString::Destroy() { gpr_free(this); }
const grpc_arg_pointer_vtable ChannelArgs::Value::int_vtable_{
// copy
[](void* p) { return p; },
// destroy
[](void*) {},
// cmp
[](void* p1, void* p2) -> int {
return QsortCompare(reinterpret_cast<intptr_t>(p1),
reinterpret_cast<intptr_t>(p2));
},
};
const grpc_arg_pointer_vtable ChannelArgs::Value::string_vtable_{
// copy
[](void* p) -> void* { return static_cast<RcString*>(p)->Ref().release(); },
// destroy
[](void* p) { static_cast<RcString*>(p)->Unref(); },
// cmp
[](void* p1, void* p2) -> int {
return QsortCompare(static_cast<RcString*>(p1)->as_string_view(),
static_cast<RcString*>(p2)->as_string_view());
},
};
ChannelArgs::Pointer::Pointer(void* p, const grpc_arg_pointer_vtable* vtable)
: p_(p), vtable_(vtable == nullptr ? EmptyVTable() : vtable) {}
@ -100,7 +139,7 @@ bool ChannelArgs::WantMinimalStack() const {
return GetBool(GRPC_ARG_MINIMAL_STACK).value_or(false);
}
ChannelArgs::ChannelArgs(AVL<std::string, Value> args)
ChannelArgs::ChannelArgs(AVL<RcStringValue, Value> args)
: args_(std::move(args)) {}
ChannelArgs ChannelArgs::Set(grpc_arg arg) const {
@ -130,52 +169,22 @@ ChannelArgs ChannelArgs::FromC(const grpc_channel_args* args) {
grpc_arg ChannelArgs::Value::MakeCArg(const char* name) const {
char* c_name = const_cast<char*>(name);
return Match(
rep_,
[c_name](int i) { return grpc_channel_arg_integer_create(c_name, i); },
[c_name](const std::shared_ptr<const std::string>& s) {
return grpc_channel_arg_string_create(c_name,
const_cast<char*>(s->c_str()));
},
[c_name](const Pointer& p) {
return grpc_channel_arg_pointer_create(c_name, p.c_pointer(),
p.c_vtable());
});
}
bool ChannelArgs::Value::operator<(const Value& rhs) const {
if (rhs.rep_.index() != rep_.index()) return rep_.index() < rhs.rep_.index();
switch (rep_.index()) {
case 0:
return absl::get<int>(rep_) < absl::get<int>(rhs.rep_);
case 1:
return *absl::get<std::shared_ptr<const std::string>>(rep_) <
*absl::get<std::shared_ptr<const std::string>>(rhs.rep_);
case 2:
return absl::get<Pointer>(rep_) < absl::get<Pointer>(rhs.rep_);
default:
Crash("unreachable");
if (rep_.c_vtable() == &int_vtable_) {
return grpc_channel_arg_integer_create(
c_name, reinterpret_cast<intptr_t>(rep_.c_pointer()));
}
}
bool ChannelArgs::Value::operator==(const Value& rhs) const {
if (rhs.rep_.index() != rep_.index()) return false;
switch (rep_.index()) {
case 0:
return absl::get<int>(rep_) == absl::get<int>(rhs.rep_);
case 1:
return *absl::get<std::shared_ptr<const std::string>>(rep_) ==
*absl::get<std::shared_ptr<const std::string>>(rhs.rep_);
case 2:
return absl::get<Pointer>(rep_) == absl::get<Pointer>(rhs.rep_);
default:
Crash("unreachable");
if (rep_.c_vtable() == &string_vtable_) {
return grpc_channel_arg_string_create(
c_name,
const_cast<char*>(static_cast<RcString*>(rep_.c_pointer())->c_str()));
}
return grpc_channel_arg_pointer_create(c_name, rep_.c_pointer(),
rep_.c_vtable());
}
ChannelArgs::CPtr ChannelArgs::ToC() const {
std::vector<grpc_arg> c_args;
args_.ForEach([&c_args](const std::string& key, const Value& value) {
args_.ForEach([&c_args](const RcStringValue& key, const Value& value) {
c_args.push_back(value.MakeCArg(key.c_str()));
});
return CPtr(static_cast<const grpc_channel_args*>(
@ -194,7 +203,7 @@ ChannelArgs ChannelArgs::Set(absl::string_view name, Value value) const {
if (const auto* p = args_.Lookup(name)) {
if (*p == value) return *this; // already have this value for this key
}
return ChannelArgs(args_.Add(std::string(name), std::move(value)));
return ChannelArgs(args_.Add(RcStringValue(name), std::move(value)));
}
ChannelArgs ChannelArgs::Set(absl::string_view name,
@ -218,8 +227,8 @@ ChannelArgs ChannelArgs::Remove(absl::string_view name) const {
ChannelArgs ChannelArgs::RemoveAllKeysWithPrefix(
absl::string_view prefix) const {
auto args = args_;
args_.ForEach([&args, prefix](const std::string& key, const Value&) {
if (absl::StartsWith(key, prefix)) args = args.Remove(key);
args_.ForEach([&](const RcStringValue& key, const Value&) {
if (absl::StartsWith(key.as_string_view(), prefix)) args = args.Remove(key);
});
return ChannelArgs(std::move(args));
}
@ -227,9 +236,7 @@ ChannelArgs ChannelArgs::RemoveAllKeysWithPrefix(
absl::optional<int> ChannelArgs::GetInt(absl::string_view name) const {
auto* v = Get(name);
if (v == nullptr) return absl::nullopt;
const auto* i = v->GetIfInt();
if (i == nullptr) return absl::nullopt;
return *i;
return v->GetIfInt();
}
absl::optional<Duration> ChannelArgs::GetDurationFromIntMillis(
@ -245,9 +252,9 @@ absl::optional<absl::string_view> ChannelArgs::GetString(
absl::string_view name) const {
auto* v = Get(name);
if (v == nullptr) return absl::nullopt;
const auto* s = v->GetIfString();
const auto s = v->GetIfString();
if (s == nullptr) return absl::nullopt;
return *s;
return s->as_string_view();
}
absl::optional<std::string> ChannelArgs::GetOwnedString(
@ -268,8 +275,8 @@ void* ChannelArgs::GetVoidPointer(absl::string_view name) const {
absl::optional<bool> ChannelArgs::GetBool(absl::string_view name) const {
auto* v = Get(name);
if (v == nullptr) return absl::nullopt;
auto* i = v->GetIfInt();
if (i == nullptr) {
auto i = v->GetIfInt();
if (!i.has_value()) {
gpr_log(GPR_ERROR, "%s ignored: it must be an integer",
std::string(name).c_str());
return absl::nullopt;
@ -286,18 +293,22 @@ absl::optional<bool> ChannelArgs::GetBool(absl::string_view name) const {
}
}
std::string ChannelArgs::Value::ToString() const {
if (rep_.c_vtable() == &int_vtable_) {
return std::to_string(reinterpret_cast<intptr_t>(rep_.c_pointer()));
}
if (rep_.c_vtable() == &string_vtable_) {
return std::string(
static_cast<RcString*>(rep_.c_pointer())->as_string_view());
}
return absl::StrFormat("%p", rep_.c_pointer());
}
std::string ChannelArgs::ToString() const {
std::vector<std::string> arg_strings;
args_.ForEach([&arg_strings](const std::string& key, const Value& value) {
std::string value_str;
if (auto* i = value.GetIfInt()) {
value_str = std::to_string(*i);
} else if (auto* s = value.GetIfString()) {
value_str = *s;
} else if (auto* p = value.GetIfPointer()) {
value_str = absl::StrFormat("%p", p->c_pointer());
}
arg_strings.push_back(absl::StrCat(key, "=", value_str));
args_.ForEach([&arg_strings](const RcStringValue& key, const Value& value) {
arg_strings.push_back(
absl::StrCat(key.as_string_view(), "=", value.ToString()));
});
return absl::StrCat("{", absl::StrJoin(arg_strings, ", "), "}");
}
@ -306,24 +317,25 @@ ChannelArgs ChannelArgs::UnionWith(ChannelArgs other) const {
if (args_.Empty()) return other;
if (other.args_.Empty()) return *this;
if (args_.Height() <= other.args_.Height()) {
args_.ForEach([&other](const std::string& key, const Value& value) {
args_.ForEach([&other](const RcStringValue& key, const Value& value) {
other.args_ = other.args_.Add(key, value);
});
return other;
} else {
auto result = *this;
other.args_.ForEach([&result](const std::string& key, const Value& value) {
if (result.args_.Lookup(key) == nullptr) {
result.args_ = result.args_.Add(key, value);
}
});
other.args_.ForEach(
[&result](const RcStringValue& key, const Value& value) {
if (result.args_.Lookup(key) == nullptr) {
result.args_ = result.args_.Add(key, value);
}
});
return result;
}
}
ChannelArgs ChannelArgs::FuzzingReferenceUnionWith(ChannelArgs other) const {
// DO NOT OPTIMIZE THIS!!
args_.ForEach([&other](const std::string& key, const Value& value) {
args_.ForEach([&other](const RcStringValue& key, const Value& value) {
other.args_ = other.args_.Add(key, value);
});
return other;

@ -22,6 +22,7 @@
#include <grpc/support/port_platform.h>
#include <stddef.h>
#include <stdint.h>
#include <algorithm> // IWYU pragma: keep
#include <iosfwd>
@ -33,7 +34,6 @@
#include "absl/meta/type_traits.h"
#include "absl/strings/string_view.h"
#include "absl/types/optional.h"
#include "absl/types/variant.h"
#include <grpc/event_engine/event_engine.h>
#include <grpc/grpc.h>
@ -220,6 +220,91 @@ struct GetObjectImpl<T, absl::enable_if_t<!WrapInSharedPtr<T>::value, void>> {
};
};
// Immutable reference counted string
class RcString {
public:
static RefCountedPtr<RcString> Make(absl::string_view src);
RefCountedPtr<RcString> Ref() {
IncrementRefCount();
return RefCountedPtr<RcString>(this);
}
void IncrementRefCount() { header_.rc.Ref(); }
void Unref() {
if (header_.rc.Unref()) Destroy();
}
absl::string_view as_string_view() const {
return absl::string_view(payload_, header_.length);
}
char* c_str() { return payload_; }
private:
explicit RcString(absl::string_view src);
void Destroy();
struct Header {
RefCount rc;
size_t length;
};
Header header_;
char payload_[];
};
// Wrapper around RefCountedPtr<RcString> to give value semantics, especially to
// overloaded operators.
class RcStringValue {
public:
RcStringValue() : str_{} {}
explicit RcStringValue(absl::string_view str) : str_(RcString::Make(str)) {}
absl::string_view as_string_view() const {
return str_ == nullptr ? absl::string_view() : str_->as_string_view();
}
const char* c_str() const { return str_ == nullptr ? "" : str_->c_str(); }
private:
RefCountedPtr<RcString> str_;
};
inline bool operator==(const RcStringValue& lhs, absl::string_view rhs) {
return lhs.as_string_view() == rhs;
}
inline bool operator==(absl::string_view lhs, const RcStringValue& rhs) {
return lhs == rhs.as_string_view();
}
inline bool operator==(const RcStringValue& lhs, const RcStringValue& rhs) {
return lhs.as_string_view() == rhs.as_string_view();
}
inline bool operator<(const RcStringValue& lhs, absl::string_view rhs) {
return lhs.as_string_view() < rhs;
}
inline bool operator<(absl::string_view lhs, const RcStringValue& rhs) {
return lhs < rhs.as_string_view();
}
inline bool operator<(const RcStringValue& lhs, const RcStringValue& rhs) {
return lhs.as_string_view() < rhs.as_string_view();
}
inline bool operator>(const RcStringValue& lhs, absl::string_view rhs) {
return lhs.as_string_view() > rhs;
}
inline bool operator>(absl::string_view lhs, const RcStringValue& rhs) {
return lhs > rhs.as_string_view();
}
inline bool operator>(const RcStringValue& lhs, const RcStringValue& rhs) {
return lhs.as_string_view() > rhs.as_string_view();
}
// Provide the canonical name for a type's channel arg key
template <typename T>
struct ChannelArgNameTraits {
@ -283,32 +368,43 @@ class ChannelArgs {
class Value {
public:
explicit Value(int n) : rep_(n) {}
explicit Value(int n) : rep_(reinterpret_cast<void*>(n), &int_vtable_) {}
explicit Value(std::string s)
: rep_(std::make_shared<const std::string>(std::move(s))) {}
: rep_(RcString::Make(s).release(), &string_vtable_) {}
explicit Value(Pointer p) : rep_(std::move(p)) {}
const int* GetIfInt() const { return absl::get_if<int>(&rep_); }
const std::string* GetIfString() const {
auto* p = absl::get_if<std::shared_ptr<const std::string>>(&rep_);
if (p == nullptr) return nullptr;
return p->get();
absl::optional<int> GetIfInt() const {
if (rep_.c_vtable() != &int_vtable_) return absl::nullopt;
return reinterpret_cast<intptr_t>(rep_.c_pointer());
}
RefCountedPtr<RcString> GetIfString() const {
if (rep_.c_vtable() != &string_vtable_) return nullptr;
return static_cast<RcString*>(rep_.c_pointer())->Ref();
}
const Pointer* GetIfPointer() const {
if (rep_.c_vtable() == &int_vtable_) return nullptr;
if (rep_.c_vtable() == &string_vtable_) return nullptr;
return &rep_;
}
const Pointer* GetIfPointer() const { return absl::get_if<Pointer>(&rep_); }
std::string ToString() const;
grpc_arg MakeCArg(const char* name) const;
bool operator<(const Value& rhs) const;
bool operator==(const Value& rhs) const;
bool operator<(const Value& rhs) const { return rep_ < rhs.rep_; }
bool operator==(const Value& rhs) const { return rep_ == rhs.rep_; }
bool operator!=(const Value& rhs) const { return !this->operator==(rhs); }
bool operator==(absl::string_view rhs) const {
auto* p = absl::get_if<std::shared_ptr<const std::string>>(&rep_);
if (p == nullptr) return false;
return **p == rhs;
auto str = GetIfString();
if (str == nullptr) return false;
return str->as_string_view() == rhs;
}
private:
absl::variant<int, std::shared_ptr<const std::string>, Pointer> rep_;
static const grpc_arg_pointer_vtable int_vtable_;
static const grpc_arg_pointer_vtable string_vtable_;
Pointer rep_;
};
struct ChannelArgsDeleter {
@ -462,12 +558,12 @@ class ChannelArgs {
std::string ToString() const;
private:
explicit ChannelArgs(AVL<std::string, Value> args);
explicit ChannelArgs(AVL<RcStringValue, Value> args);
GRPC_MUST_USE_RESULT ChannelArgs Set(absl::string_view name,
Value value) const;
AVL<std::string, Value> args_;
AVL<RcStringValue, Value> args_;
};
std::ostream& operator<<(std::ostream& out, const ChannelArgs& args);

@ -32,6 +32,7 @@
#include "src/core/lib/channel/channel_args.h"
#include "src/core/lib/debug/trace.h"
#include "src/core/lib/gprpp/crash.h"
#include "src/core/lib/gprpp/ref_counted_ptr.h"
#include "src/core/lib/surface/api_trace.h"
namespace grpc_core {
@ -227,11 +228,13 @@ absl::optional<grpc_compression_algorithm>
DefaultCompressionAlgorithmFromChannelArgs(const ChannelArgs& args) {
auto* value = args.Get(GRPC_COMPRESSION_CHANNEL_DEFAULT_ALGORITHM);
if (value == nullptr) return absl::nullopt;
if (auto* p = value->GetIfInt()) {
return static_cast<grpc_compression_algorithm>(*p);
auto ival = value->GetIfInt();
if (ival.has_value()) {
return static_cast<grpc_compression_algorithm>(*ival);
}
if (auto* p = value->GetIfString()) {
return ParseCompressionAlgorithm(*p);
auto sval = value->GetIfString();
if (sval != nullptr) {
return ParseCompressionAlgorithm(sval->as_string_view());
}
return absl::nullopt;
}

@ -45,12 +45,14 @@ class RefCount {
public:
using Value = intptr_t;
RefCount() : RefCount(1) {}
// `init` is the initial refcount stored in this object.
//
// `trace` is a string to be logged with trace events; if null, no
// trace logging will be done. Tracing is a no-op in non-debug builds.
explicit RefCount(
Value init = 1,
Value init,
const char*
#ifndef NDEBUG
// Leave unnamed if NDEBUG to avoid unused parameter warning

Loading…
Cancel
Save