tint: Implement Switch() without recursion

This removes the number of function calls made in non-optimized builds.

Reduces the optimized, all-features-enabled 'tint' executable size by
about 1%.

This change removes the bloom filter optimizations which provided
substantial performance gains with the old recursive implementation,
however this still appears to be ~1% faster than the optimized version.

Change-Id: Ic2bb82e9182459e37907f9e0d0b4771bde218f9f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/123440
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Commit-Queue: Ben Clayton <bclayton@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Kokoro: Ben Clayton <bclayton@chromium.org>
This commit is contained in:
Ben Clayton 2023-03-09 18:24:19 +00:00 committed by Dawn LUCI CQ
parent 23946b3606
commit 4fea9d0fbb
6 changed files with 92 additions and 134 deletions

View File

@ -21,9 +21,7 @@
#include <utility>
#include "src/tint/traits.h"
#include "src/tint/utils/bitcast.h"
#include "src/tint/utils/crc32.h"
#include "src/tint/utils/defer.h"
#if defined(__clang__)
/// Temporarily disable certain warnings when using Castable API

View File

@ -42,6 +42,7 @@
#include "src/tint/type/multisampled_texture.h"
#include "src/tint/type/sampled_texture.h"
#include "src/tint/type/texture_dimension.h"
#include "src/tint/utils/defer.h"
#include "src/tint/utils/reverse.h"
#include "src/tint/utils/string.h"
#include "src/tint/utils/string_stream.h"

View File

@ -39,6 +39,7 @@
#include "src/tint/sem/while_statement.h"
#include "src/tint/switch.h"
#include "src/tint/utils/block_allocator.h"
#include "src/tint/utils/defer.h"
#include "src/tint/utils/map.h"
#include "src/tint/utils/string_stream.h"
#include "src/tint/utils/unique_vector.h"

View File

@ -19,6 +19,8 @@
#include <utility>
#include "src/tint/castable.h"
#include "src/tint/utils/bitcast.h"
#include "src/tint/utils/defer.h"
namespace tint {
@ -62,126 +64,6 @@ constexpr int IndexOfDefaultCase() {
}
}
/// The implementation of Switch() for non-Default cases.
/// Switch splits the cases into two a low and high block of cases, and quickly rules out blocks
/// that cannot match by comparing the HashCode of the object and the cases in the block. If a block
/// of cases may match the given object's type, then that block is split into two, and the process
/// recurses. When NonDefaultCases() is called with a single case, then As<> will be used to
/// dynamically cast to the case type and if the cast succeeds, then the case handler is called.
/// @returns true if a case handler was found, otherwise false.
template <typename T, typename RETURN_TYPE, typename... CASES>
inline bool NonDefaultCases([[maybe_unused]] T* object,
const TypeInfo* type,
[[maybe_unused]] RETURN_TYPE* result,
std::tuple<CASES...>&& cases) {
using Cases = std::tuple<CASES...>;
static constexpr bool kHasReturnType = !std::is_same_v<RETURN_TYPE, void>;
static constexpr size_t kNumCases = sizeof...(CASES);
if constexpr (kNumCases == 0) {
// No cases. Nothing to do.
return false;
} else if constexpr (kNumCases == 1) { // NOLINT: cpplint doesn't understand
// `else if constexpr`
// Single case.
using CaseFunc = std::tuple_element_t<0, Cases>;
static_assert(!IsDefaultCase<CaseFunc>, "NonDefaultCases called with a Default case");
// Attempt to dynamically cast the object to the handler type. If that succeeds, call the
// case handler with the cast object.
using CaseType = SwitchCaseType<CaseFunc>;
if (type->Is<CaseType>()) {
auto* ptr = static_cast<CaseType*>(object);
if constexpr (kHasReturnType) {
new (result) RETURN_TYPE(static_cast<RETURN_TYPE>(std::get<0>(cases)(ptr)));
} else {
std::get<0>(cases)(ptr);
}
return true;
}
return false;
} else {
// Multiple cases.
// Check the hashcode bits to see if there's any possibility of a case matching in these
// cases. If there isn't, we can skip all these cases.
if (MaybeAnyOf(TypeInfo::CombinedHashCodeOf<SwitchCaseType<CASES>...>(),
type->full_hashcode)) {
// Split the cases into two, and recurse.
constexpr size_t kMid = kNumCases / 2;
return NonDefaultCases(object, type, result, traits::Slice<0, kMid>(cases)) ||
NonDefaultCases(object, type, result,
traits::Slice<kMid, kNumCases - kMid>(cases));
} else {
return false;
}
}
}
/// The implementation of Switch() for all cases.
/// @see NonDefaultCases
template <typename T, typename RETURN_TYPE, typename... CASES>
inline void SwitchCases(T* object, RETURN_TYPE* result, std::tuple<CASES...>&& cases) {
using Cases = std::tuple<CASES...>;
static constexpr int kDefaultIndex = detail::IndexOfDefaultCase<Cases>();
static constexpr bool kHasDefaultCase = kDefaultIndex >= 0;
static constexpr bool kHasReturnType = !std::is_same_v<RETURN_TYPE, void>;
// Static assertions
static constexpr bool kDefaultIsOK =
kDefaultIndex == -1 || kDefaultIndex == static_cast<int>(std::tuple_size_v<Cases> - 1);
static constexpr bool kReturnIsOK =
kHasDefaultCase || !kHasReturnType || std::is_constructible_v<RETURN_TYPE>;
static_assert(kDefaultIsOK, "Default case must be last in Switch()");
static_assert(kReturnIsOK,
"Switch() requires either a Default case or a return type that is either void or "
"default-constructable");
// If the static asserts have fired, don't bother spewing more errors below
static constexpr bool kAllOK = kDefaultIsOK && kReturnIsOK;
if constexpr (kAllOK) {
if (object) {
auto* type = &object->TypeInfo();
if constexpr (kHasDefaultCase) {
// Evaluate non-default cases.
if (!detail::NonDefaultCases<T>(object, type, result,
traits::Slice<0, kDefaultIndex>(cases))) {
// Nothing matched. Evaluate default case.
if constexpr (kHasReturnType) {
new (result) RETURN_TYPE(
static_cast<RETURN_TYPE>(std::get<kDefaultIndex>(cases)({})));
} else {
std::get<kDefaultIndex>(cases)({});
}
}
} else {
if (!detail::NonDefaultCases<T>(object, type, result, std::move(cases))) {
// Nothing matched. No default case.
if constexpr (kHasReturnType) {
new (result) RETURN_TYPE();
}
}
}
} else {
// Object is nullptr, so no cases can match
if constexpr (kHasDefaultCase) {
// Evaluate default case.
if constexpr (kHasReturnType) {
new (result)
RETURN_TYPE(static_cast<RETURN_TYPE>(std::get<kDefaultIndex>(cases)({})));
} else {
std::get<kDefaultIndex>(cases)({});
}
} else {
// No default case, no case can match.
if constexpr (kHasReturnType) {
new (result) RETURN_TYPE();
}
}
}
}
}
/// Resolves to T if T is not nullptr_t, otherwise resolves to Ignore.
template <typename T>
using NullptrToIgnore = std::conditional_t<std::is_same_v<T, std::nullptr_t>, Ignore, T>;
@ -282,21 +164,95 @@ namespace tint {
template <typename RETURN_TYPE = detail::Infer, typename T = CastableBase, typename... CASES>
inline auto Switch(T* object, CASES&&... cases) {
using ReturnType = detail::SwitchReturnType<RETURN_TYPE, traits::ReturnType<CASES>...>;
static constexpr int kDefaultIndex = detail::IndexOfDefaultCase<std::tuple<CASES...>>();
static constexpr bool kHasDefaultCase = kDefaultIndex >= 0;
static constexpr bool kHasReturnType = !std::is_same_v<ReturnType, void>;
// Static assertions
static constexpr bool kDefaultIsOK =
kDefaultIndex == -1 || kDefaultIndex == static_cast<int>(sizeof...(CASES) - 1);
static constexpr bool kReturnIsOK =
kHasDefaultCase || !kHasReturnType || std::is_constructible_v<ReturnType>;
static_assert(kDefaultIsOK, "Default case must be last in Switch()");
static_assert(kReturnIsOK,
"Switch() requires either a Default case or a return type that is either void or "
"default-constructable");
if (!object) { // Object is nullptr, so no cases can match
if constexpr (kHasDefaultCase) {
// Evaluate default case.
auto&& default_case =
std::get<kDefaultIndex>(std::forward_as_tuple(std::forward<CASES>(cases)...));
return static_cast<ReturnType>(default_case(Default{}));
} else {
// No default case, no case can match.
if constexpr (kHasReturnType) {
return ReturnType{};
} else {
return;
}
}
}
// Replacement for std::aligned_storage as this is broken on earlier versions of MSVC.
using ReturnTypeOrU8 = std::conditional_t<kHasReturnType, ReturnType, uint8_t>;
struct alignas(alignof(ReturnTypeOrU8)) ReturnStorage {
uint8_t data[sizeof(ReturnTypeOrU8)];
};
ReturnStorage storage;
auto* result = utils::Bitcast<ReturnTypeOrU8*>(&storage);
const TypeInfo& type_info = object->TypeInfo();
// Examines the parameter type of the case function.
// If the parameter is a pointer type that `object` is of, or derives from, then that case
// function is called with `object` cast to that type, and `try_case` returns true.
// If the parameter is of type `Default`, then that case function is called and `try_case`
// returns true.
// Otherwise `try_case` returns false.
// If the case function is called and it returns a value, then this is copy constructed to the
// `result` pointer.
auto try_case = [&](auto&& case_fn) {
using CaseFunc = std::decay_t<decltype(case_fn)>;
using CaseType = detail::SwitchCaseType<CaseFunc>;
if constexpr (std::is_same_v<CaseType, Default>) {
if constexpr (kHasReturnType) {
new (result) ReturnType(static_cast<ReturnType>(case_fn(Default{})));
} else {
case_fn(Default{});
}
return true;
} else {
if (type_info.Is<CaseType>()) {
auto* v = static_cast<CaseType*>(object);
if constexpr (kHasReturnType) {
new (result) ReturnType(static_cast<ReturnType>(case_fn(v)));
} else {
case_fn(v);
}
return true;
}
}
return false;
};
// Use a logical-or fold expression to try each of the cases in turn, until one matches the
// object type or a Default is reached. `handled` is true if a case function was called.
bool handled = ((try_case(std::forward<CASES>(cases)) || ...));
if constexpr (kHasReturnType) {
// Replacement for std::aligned_storage as this is broken on earlier versions of MSVC.
struct alignas(alignof(ReturnType)) ReturnStorage {
uint8_t data[sizeof(ReturnType)];
};
ReturnStorage storage;
auto* res = utils::Bitcast<ReturnType*>(&storage);
TINT_DEFER(res->~ReturnType());
detail::SwitchCases(object, res, std::forward_as_tuple(std::forward<CASES>(cases)...));
return *res;
} else {
detail::SwitchCases<T, void>(object, nullptr,
std::forward_as_tuple(std::forward<CASES>(cases)...));
if constexpr (kHasDefaultCase) {
// Default case means there must be a returned value.
// No need to check handled, no requirement for a zero-initializer of ReturnType.
TINT_DEFER(result->~ReturnType());
return *result;
} else {
if (handled) {
TINT_DEFER(result->~ReturnType());
return *result;
}
return ReturnType{};
}
}
}

View File

@ -20,6 +20,7 @@
#include "src/tint/castable.h"
#include "src/tint/traits.h"
#include "src/tint/utils/bitcast.h"
namespace tint::utils {

View File

@ -35,6 +35,7 @@
#include "src/tint/sem/struct.h"
#include "src/tint/sem/switch_statement.h"
#include "src/tint/switch.h"
#include "src/tint/utils/defer.h"
#include "src/tint/utils/math.h"
#include "src/tint/utils/scoped_assignment.h"
#include "src/tint/writer/float_to_string.h"