diff options
-rw-r--r-- | CMake/AbseilDll.cmake | 1 | ||||
-rw-r--r-- | absl/base/BUILD.bazel | 37 | ||||
-rw-r--r-- | absl/base/CMakeLists.txt | 25 | ||||
-rw-r--r-- | absl/base/no_destructor.h | 217 | ||||
-rw-r--r-- | absl/base/no_destructor_benchmark.cc | 165 | ||||
-rw-r--r-- | absl/base/no_destructor_test.cc | 209 |
6 files changed, 654 insertions, 0 deletions
diff --git a/CMake/AbseilDll.cmake b/CMake/AbseilDll.cmake index 1177d5d0..19272795 100644 --- a/CMake/AbseilDll.cmake +++ b/CMake/AbseilDll.cmake @@ -55,6 +55,7 @@ set(ABSL_INTERNAL_DLL_FILES "base/log_severity.cc" "base/log_severity.h" "base/macros.h" + "base/no_destructor.h" "base/nullability.h" "base/optimization.h" "base/options.h" diff --git a/absl/base/BUILD.bazel b/absl/base/BUILD.bazel index daa10af6..60d05a83 100644 --- a/absl/base/BUILD.bazel +++ b/absl/base/BUILD.bazel @@ -70,6 +70,14 @@ cc_library( ) cc_library( + name = "no_destructor", + hdrs = ["no_destructor.h"], + copts = ABSL_DEFAULT_COPTS, + linkopts = ABSL_DEFAULT_LINKOPTS, + deps = [":config"], +) + +cc_library( name = "nullability", srcs = ["internal/nullability_impl.h"], hdrs = ["nullability.h"], @@ -584,6 +592,35 @@ cc_test( ) cc_test( + name = "no_destructor_test", + srcs = ["no_destructor_test.cc"], + copts = ABSL_TEST_COPTS, + linkopts = ABSL_DEFAULT_LINKOPTS, + deps = [ + ":config", + ":no_destructor", + ":raw_logging_internal", + "@com_google_googletest//:gtest", + "@com_google_googletest//:gtest_main", + ], +) + +cc_binary( + name = "no_destructor_benchmark", + testonly = 1, + srcs = ["no_destructor_benchmark.cc"], + copts = ABSL_TEST_COPTS, + linkopts = ABSL_DEFAULT_LINKOPTS, + tags = ["benchmark"], + visibility = ["//visibility:private"], + deps = [ + ":no_destructor", + ":raw_logging_internal", + "@com_github_google_benchmark//:benchmark_main", + ], +) + +cc_test( name = "nullability_test", srcs = ["nullability_test.cc"], deps = [ diff --git a/absl/base/CMakeLists.txt b/absl/base/CMakeLists.txt index 3f4a1c42..9ca5cf8b 100644 --- a/absl/base/CMakeLists.txt +++ b/absl/base/CMakeLists.txt @@ -57,6 +57,17 @@ absl_cc_library( absl_cc_library( NAME + no_destructor + HDRS + "no_destructor.h" + DEPS + absl::config + COPTS + ${ABSL_DEFAULT_COPTS} +) + +absl_cc_library( + NAME nullability HDRS "nullability.h" @@ -510,6 +521,20 @@ absl_cc_test( absl_cc_test( NAME + no_destructor_test + SRCS + "no_destructor_test.cc" + COPTS + ${ABSL_TEST_COPTS} + DEPS + absl::no_destructor + absl::config + absl::raw_logging_internal + GTest::gtest_main +) + +absl_cc_test( + NAME raw_logging_test SRCS "raw_logging_test.cc" diff --git a/absl/base/no_destructor.h b/absl/base/no_destructor.h new file mode 100644 index 00000000..d4b16a6e --- /dev/null +++ b/absl/base/no_destructor.h @@ -0,0 +1,217 @@ +// Copyright 2023 The Abseil Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ----------------------------------------------------------------------------- +// File: no_destructor.h +// ----------------------------------------------------------------------------- +// +// This header file defines the absl::NoDestructor<T> wrapper for defining a +// static type that does not need to be destructed upon program exit. Instead, +// such an object survives during program exit (and can be safely accessed at +// any time). +// +// Objects of such type, if constructed safely and under the right conditions, +// provide two main benefits over other alternatives: +// +// * Global objects not normally allowed due to concerns of destruction order +// (i.e. no "complex globals") can be safely allowed, provided that such +// objects can be constant initialized. +// * Function scope static objects can be optimized to avoid heap allocation, +// pointer chasing, and allow lazy construction. +// +// See below for complete details. + + +#ifndef ABSL_BASE_NO_DESTRUCTOR_H_ +#define ABSL_BASE_NO_DESTRUCTOR_H_ + +#include <new> +#include <type_traits> +#include <utility> + +#include "absl/base/config.h" + +namespace absl { +ABSL_NAMESPACE_BEGIN + +// absl::NoDestructor<T> +// +// NoDestructor<T> is a wrapper around an object of type T that behaves as an +// object of type T but never calls T's destructor. NoDestructor<T> makes it +// safer and/or more efficient to use such objects in static storage contexts: +// as global or function scope static variables. +// +// An instance of absl::NoDestructor<T> has similar type semantics to an +// instance of T: +// +// * Constructs in the same manner as an object of type T through perfect +// forwarding. +// * Provides pointer/reference semantic access to the object of type T via +// `->`, `*`, and `get()`. +// (Note that `const NoDestructor<T>` works like a pointer to const `T`.) +// +// An object of type NoDestructor<T> should be defined in static storage: +// as either a global static object, or as a function scope static variable. +// +// Additionally, NoDestructor<T> provides the following benefits: +// +// * Never calls T's destructor for the object +// * If the object is a function-local static variable, the type can be +// lazily constructed. +// +// An object of type NoDestructor<T> is "trivially destructible" in the notion +// that its destructor is never run. Provided that an object of this type can be +// safely initialized and does not need to be cleaned up on program shutdown, +// NoDestructor<T> allows you to define global static variables, since Google's +// C++ style guide ban on such objects doesn't apply to objects that are +// trivially destructible. +// +// Usage as Global Static Variables +// +// NoDestructor<T> allows declaration of a global object with a non-trivial +// constructor in static storage without needing to add a destructor. +// However, such objects still need to worry about initialization order, so +// such objects should be const initialized: +// +// // Global or namespace scope. +// ABSL_CONST_INIT absl::NoDestructor<MyRegistry> reg{"foo", "bar", 8008}; +// +// Note that if your object already has a trivial destructor, you don't need to +// use NoDestructor<T>. +// +// Usage as Function Scope Static Variables +// +// Function static objects will be lazily initialized within static storage: +// +// // Function scope. +// const std::string& MyString() { +// static const absl::NoDestructor<std::string> x("foo"); +// return *x; +// } +// +// For function static variables, NoDestructor avoids heap allocation and can be +// inlined in static storage, resulting in exactly-once, thread-safe +// construction of an object, and very fast access thereafter (the cost is a few +// extra cycles). +// +// Using NoDestructor<T> in this manner is generally better than other patterns +// which require pointer chasing: +// +// // Prefer using absl::NoDestructor<T> instead for the static variable. +// const std::string& MyString() { +// static const std::string* x = new std::string("foo"); +// return *x; +// } +// +template <typename T> +class NoDestructor { + public: + // Forwards arguments to the T's constructor: calls T(args...). + template <typename... Ts, + // Disable this overload when it might collide with copy/move. + typename std::enable_if<!std::is_same<void(std::decay_t<Ts>&...), + void(NoDestructor&)>::value, + int>::type = 0> + explicit constexpr NoDestructor(Ts&&... args) + : impl_(std::forward<Ts>(args)...) {} + + // Forwards copy and move construction for T. Enables usage like this: + // static NoDestructor<std::array<string, 3>> x{{{"1", "2", "3"}}}; + // static NoDestructor<std::vector<int>> x{{1, 2, 3}}; + explicit constexpr NoDestructor(const T& x) : impl_(x) {} + explicit constexpr NoDestructor(T&& x) + : impl_(std::move(x)) {} + + // No copying. + NoDestructor(const NoDestructor&) = delete; + NoDestructor& operator=(const NoDestructor&) = delete; + + // Pretend to be a smart pointer to T with deep constness. + // Never returns a null pointer. + T& operator*() { return *get(); } + T* operator->() { return get(); } + T* get() { return impl_.get(); } + const T& operator*() const { return *get(); } + const T* operator->() const { return get(); } + const T* get() const { return impl_.get(); } + + private: + class DirectImpl { + public: + template <typename... Args> + explicit constexpr DirectImpl(Args&&... args) + : value_(std::forward<Args>(args)...) {} + const T* get() const { return &value_; } + T* get() { return &value_; } + + private: + T value_; + }; + + class PlacementImpl { + public: + template <typename... Args> + explicit PlacementImpl(Args&&... args) { + new (&space_) T(std::forward<Args>(args)...); + } + const T* get() const { + return Launder(reinterpret_cast<const T*>(&space_)); + } + T* get() { return Launder(reinterpret_cast<T*>(&space_)); } + + private: + template <typename P> + static P* Launder(P* p) { +#if defined(__cpp_lib_launder) && __cpp_lib_launder >= 201606L + return std::launder(p); +#elif ABSL_HAVE_BUILTIN(__builtin_launder) + return __builtin_launder(p); +#else + // When `std::launder` or equivalent are not available, we rely on + // undefined behavior, which works as intended on Abseil's officially + // supported platforms as of Q3 2023. +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstrict-aliasing" +#endif + return p; +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif +#endif + } + + alignas(T) unsigned char space_[sizeof(T)]; + }; + + // If the object is trivially destructible we use a member directly to avoid + // potential once-init runtime initialization. It somewhat defeats the + // purpose of NoDestructor in this case, but this makes the class more + // friendly to generic code. + std::conditional_t<std::is_trivially_destructible<T>::value, DirectImpl, + PlacementImpl> + impl_; +}; + +#ifdef ABSL_HAVE_CLASS_TEMPLATE_ARGUMENT_DEDUCTION +// Provide 'Class Template Argument Deduction': the type of NoDestructor's T +// will be the same type as the argument passed to NoDestructor's constructor. +template <typename T> +NoDestructor(T) -> NoDestructor<T>; +#endif // ABSL_HAVE_CLASS_TEMPLATE_ARGUMENT_DEDUCTION + +ABSL_NAMESPACE_END +} // namespace absl + +#endif // ABSL_BASE_NO_DESTRUCTOR_H_ diff --git a/absl/base/no_destructor_benchmark.cc b/absl/base/no_destructor_benchmark.cc new file mode 100644 index 00000000..5fc88f1d --- /dev/null +++ b/absl/base/no_destructor_benchmark.cc @@ -0,0 +1,165 @@ +// Copyright 2023 The Abseil Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <cstdint> + +#include "absl/base/internal/raw_logging.h" +#include "absl/base/no_destructor.h" +#include "benchmark/benchmark.h" + +namespace { + +// Number of static-NoDestructor-in-a-function to exercise. +// This must be low enough not to hit template instantiation limits +// (happens around 1000). +constexpr int kNumObjects = 1; // set to 512 when doing benchmarks + // 1 is faster to compile: just one templated + // function instantiation + +// Size of individual objects to benchmark static-NoDestructor-in-a-function +// usage with. +constexpr int kObjSize = sizeof(void*)*1; + +// Simple object of kObjSize bytes (rounded to int). +// We benchmark complete reading of its state via Verify(). +class BM_Blob { + public: + BM_Blob(int val) { for (auto& d : data_) d = val; } + BM_Blob() : BM_Blob(-1) {} + void Verify(int val) const { // val must be the c-tor argument + for (auto& d : data_) ABSL_INTERNAL_CHECK(d == val, ""); + } + private: + int data_[kObjSize / sizeof(int) > 0 ? kObjSize / sizeof(int) : 1]; +}; + +// static-NoDestructor-in-a-function pattern instances. +// We'll instantiate kNumObjects of them. +template<int i> +const BM_Blob& NoDestrBlobFunc() { + static absl::NoDestructor<BM_Blob> x(i); + return *x; +} + +// static-heap-ptr-in-a-function pattern instances +// We'll instantiate kNumObjects of them. +template<int i> +const BM_Blob& OnHeapBlobFunc() { + static BM_Blob* x = new BM_Blob(i); + return *x; +} + +// Type for NoDestrBlobFunc or OnHeapBlobFunc. +typedef const BM_Blob& (*FuncType)(); + +// ========================================================================= // +// Simple benchmarks that read a single BM_Blob over and over, hence +// all they touch fits into L1 CPU cache: + +// Direct non-POD global variable (style guide violation) as a baseline. +static BM_Blob direct_blob(0); + +void BM_Direct(benchmark::State& state) { + for (auto s : state) { + direct_blob.Verify(0); + } +} +BENCHMARK(BM_Direct); + +void BM_NoDestr(benchmark::State& state) { + for (auto s : state) { + NoDestrBlobFunc<0>().Verify(0); + } +} +BENCHMARK(BM_NoDestr); + +void BM_OnHeap(benchmark::State& state) { + for (auto s : state) { + OnHeapBlobFunc<0>().Verify(0); + } +} +BENCHMARK(BM_OnHeap); + +// ========================================================================= // +// Benchmarks that read kNumObjects of BM_Blob over and over, hence with +// appropriate values of sizeof(BM_Blob) and kNumObjects their working set +// can exceed a given layer of CPU cache. + +// Type of benchmark to select between NoDestrBlobFunc and OnHeapBlobFunc. +enum BM_Type { kNoDestr, kOnHeap, kDirect }; + +// BlobFunc<n>(t, i) returns the i-th function of type t. +// n must be larger than i (we'll use kNumObjects for n). +template<int n> +FuncType BlobFunc(BM_Type t, int i) { + if (i == n) { + switch (t) { + case kNoDestr: return &NoDestrBlobFunc<n>; + case kOnHeap: return &OnHeapBlobFunc<n>; + case kDirect: return nullptr; + } + } + return BlobFunc<n-1>(t, i); +} + +template<> +FuncType BlobFunc<0>(BM_Type t, int i) { + ABSL_INTERNAL_CHECK(i == 0, ""); + switch (t) { + case kNoDestr: return &NoDestrBlobFunc<0>; + case kOnHeap: return &OnHeapBlobFunc<0>; + case kDirect: return nullptr; + } + return nullptr; +} + +// Direct non-POD global variables (style guide violation) as a baseline. +static BM_Blob direct_blobs[kNumObjects]; + +// Helper that cheaply maps benchmark iteration to randomish index in +// [0, kNumObjects). +int RandIdx(int i) { + // int64 is to avoid overflow and generating negative return values: + return (static_cast<int64_t>(i) * 13) % kNumObjects; +} + +// Generic benchmark working with kNumObjects for any of the possible BM_Type. +template <BM_Type t> +void BM_Many(benchmark::State& state) { + FuncType funcs[kNumObjects]; + for (int i = 0; i < kNumObjects; ++i) { + funcs[i] = BlobFunc<kNumObjects-1>(t, i); + } + if (t == kDirect) { + for (auto s : state) { + int idx = RandIdx(state.iterations()); + direct_blobs[idx].Verify(-1); + } + } else { + for (auto s : state) { + int idx = RandIdx(state.iterations()); + funcs[idx]().Verify(idx); + } + } +} + +void BM_DirectMany(benchmark::State& state) { BM_Many<kDirect>(state); } +void BM_NoDestrMany(benchmark::State& state) { BM_Many<kNoDestr>(state); } +void BM_OnHeapMany(benchmark::State& state) { BM_Many<kOnHeap>(state); } + +BENCHMARK(BM_DirectMany); +BENCHMARK(BM_NoDestrMany); +BENCHMARK(BM_OnHeapMany); + +} // namespace diff --git a/absl/base/no_destructor_test.cc b/absl/base/no_destructor_test.cc new file mode 100644 index 00000000..71693c7e --- /dev/null +++ b/absl/base/no_destructor_test.cc @@ -0,0 +1,209 @@ +// Copyright 2023 The Abseil Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "absl/base/no_destructor.h" + +#include <array> +#include <initializer_list> +#include <string> +#include <type_traits> +#include <vector> + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/base/config.h" +#include "absl/base/internal/raw_logging.h" + +namespace { + +struct Blob { + Blob() : val(42) {} + Blob(int x, int y) : val(x + y) {} + Blob(std::initializer_list<int> xs) { + val = 0; + for (auto& x : xs) val += x; + } + + Blob(const Blob& /*b*/) = delete; + Blob(Blob&& b) noexcept : val(b.val) { + b.moved_out = true; + } // moving is fine + + // no crash: NoDestructor indeed does not destruct (the moved-out Blob + // temporaries do get destroyed though) + ~Blob() { ABSL_INTERNAL_CHECK(moved_out, "~Blob"); } + + int val; + bool moved_out = false; +}; + +struct TypeWithDeletedDestructor { + ~TypeWithDeletedDestructor() = delete; +}; + +TEST(NoDestructorTest, DestructorNeverCalled) { + absl::NoDestructor<TypeWithDeletedDestructor> a; + (void)a; +} + +TEST(NoDestructorTest, Noncopyable) { + using T = absl::NoDestructor<int>; + + EXPECT_FALSE((std::is_constructible<T, T>::value)); + EXPECT_FALSE((std::is_constructible<T, const T>::value)); + EXPECT_FALSE((std::is_constructible<T, T&>::value)); + EXPECT_FALSE((std::is_constructible<T, const T&>::value)); + + EXPECT_FALSE((std::is_assignable<T&, T>::value)); + EXPECT_FALSE((std::is_assignable<T&, const T>::value)); + EXPECT_FALSE((std::is_assignable<T&, T&>::value)); + EXPECT_FALSE((std::is_assignable<T&, const T&>::value)); +} + +TEST(NoDestructorTest, Interface) { + EXPECT_TRUE(std::is_trivially_destructible<absl::NoDestructor<Blob>>::value); + EXPECT_TRUE( + std::is_trivially_destructible<absl::NoDestructor<const Blob>>::value); + { + absl::NoDestructor<Blob> b; // default c-tor + // access: *, ->, get() + EXPECT_EQ(42, (*b).val); + (*b).val = 55; + EXPECT_EQ(55, b->val); + b->val = 66; + EXPECT_EQ(66, b.get()->val); + b.get()->val = 42; // NOLINT + EXPECT_EQ(42, (*b).val); + } + { + absl::NoDestructor<const Blob> b(70, 7); // regular c-tor, const + EXPECT_EQ(77, (*b).val); + EXPECT_EQ(77, b->val); + EXPECT_EQ(77, b.get()->val); + } + { + const absl::NoDestructor<Blob> b{ + {20, 28, 40}}; // init-list c-tor, deep const + // This only works in clang, not in gcc: + // const absl::NoDestructor<Blob> b({20, 28, 40}); + EXPECT_EQ(88, (*b).val); + EXPECT_EQ(88, b->val); + EXPECT_EQ(88, b.get()->val); + } +} + +TEST(NoDestructorTest, SfinaeRegressionAbstractArg) { + struct Abstract { + virtual ~Abstract() = default; + virtual int foo() const = 0; + }; + + struct Concrete : Abstract { + int foo() const override { return 17; } + }; + + struct UsesAbstractInConstructor { + explicit UsesAbstractInConstructor(const Abstract& abstract) + : i(abstract.foo()) {} + int i; + }; + + Concrete input; + absl::NoDestructor<UsesAbstractInConstructor> foo1(input); + EXPECT_EQ(foo1->i, 17); + absl::NoDestructor<UsesAbstractInConstructor> foo2( + static_cast<const Abstract&>(input)); + EXPECT_EQ(foo2->i, 17); +} + +// ========================================================================= // + +std::string* Str0() { + static absl::NoDestructor<std::string> x; + return x.get(); +} + +extern const std::string& Str2(); + +const char* Str1() { + static absl::NoDestructor<std::string> x(Str2() + "_Str1"); + return x->c_str(); +} + +const std::string& Str2() { + static absl::NoDestructor<std::string> x("Str2"); + return *x; +} + +const std::string& Str2Copy() { + // Exercise copy construction + static absl::NoDestructor<std::string> x(Str2()); + return *x; +} + +typedef std::array<std::string, 3> MyArray; +const MyArray& Array() { + static absl::NoDestructor<MyArray> x{{{"foo", "bar", "baz"}}}; + // This only works in clang, not in gcc: + // static absl::NoDestructor<MyArray> x({{"foo", "bar", "baz"}}); + return *x; +} + +typedef std::vector<int> MyVector; +const MyVector& Vector() { + static absl::NoDestructor<MyVector> x{{1, 2, 3}}; + return *x; +} + +const int& Int() { + static absl::NoDestructor<int> x; + return *x; +} + +TEST(NoDestructorTest, StaticPattern) { + EXPECT_TRUE( + std::is_trivially_destructible<absl::NoDestructor<std::string>>::value); + EXPECT_TRUE( + std::is_trivially_destructible<absl::NoDestructor<MyArray>>::value); + EXPECT_TRUE( + std::is_trivially_destructible<absl::NoDestructor<MyVector>>::value); + EXPECT_TRUE(std::is_trivially_destructible<absl::NoDestructor<int>>::value); + + EXPECT_EQ(*Str0(), ""); + Str0()->append("foo"); + EXPECT_EQ(*Str0(), "foo"); + + EXPECT_EQ(std::string(Str1()), "Str2_Str1"); + + EXPECT_EQ(Str2(), "Str2"); + EXPECT_EQ(Str2Copy(), "Str2"); + + EXPECT_THAT(Array(), testing::ElementsAre("foo", "bar", "baz")); + + EXPECT_THAT(Vector(), testing::ElementsAre(1, 2, 3)); + + EXPECT_EQ(0, Int()); // should get zero-initialized +} + +#ifdef ABSL_HAVE_CLASS_TEMPLATE_ARGUMENT_DEDUCTION +// This would fail to compile if Class Template Argument Deduction was not +// provided for absl::NoDestructor. +TEST(NoDestructorTest, ClassTemplateArgumentDeduction) { + absl::NoDestructor i(1); + static_assert(std::is_same<decltype(i), absl::NoDestructor<int>>::value, + "Expected deduced type to be int."); +} +#endif + +} // namespace |