This switches `DCHECK` and `FATAL` as well. The goal is to reduce the code size impact of these assertions so that we can keep more of them enabled. Currently, the largest cost I see from `CHECK` is not the actual check or the cold code itself, but actually the failure to inline trivial functions due to the presence of the cold code. This means that our goal isn't to reduce apparent code size in the final binary but the LLVM IR cost assessed for these routines in the inliner, which closely correlates with code size but is a bit different. As discussed in #4283, experimentation shows that a single function call with a minimal number of arguments is the lowest cost model for these. This is easily achieved with a format-string API that internally uses `llvm::formatv`. This PR is essentially the `CHECK` version of #4283. However, the check macros are substantially harder to make work with both format strings and streaming because they also take a condition. Also, unexpectedly, I was very successful at devising a regular expression based automated rewrite from the streaming to the format string form with only low 10s of manual fixes. This includes compacting strings broken up across lines, etc. Given how well that went, I've prepared this PR which just directly switches to the format string API and migrate everything to use it. One nice side-effect is that the format string approach ends up greatly simplifying the implementation here as well. This is ... *shockingly* effective. Parsing speeds up by more than 3% with just this change. And checking speeds up by **8%** with this change alone: ``` BM_CompileAPIFileDenseDecls<Phase::Parse>/256 86.3µs ± 1% 82.9µs ± 1% -3.94% (p=0.000 n=17+19) BM_CompileAPIFileDenseDecls<Phase::Parse>/1024 431µs ± 1% 415µs ± 1% -3.76% (p=0.000 n=18+19) BM_CompileAPIFileDenseDecls<Phase::Parse>/4096 1.77ms ± 1% 1.71ms ± 1% -3.18% (p=0.000 n=18+19) BM_CompileAPIFileDenseDecls<Phase::Parse>/16384 7.44ms ± 1% 7.17ms ± 2% -3.56% (p=0.000 n=18+20) BM_CompileAPIFileDenseDecls<Phase::Parse>/65536 30.7ms ± 1% 29.7ms ± 1% -3.15% (p=0.000 n=18+20) BM_CompileAPIFileDenseDecls<Phase::Parse>/262144 131ms ± 1% 127ms ± 1% -2.81% (p=0.000 n=18+18) BM_CompileAPIFileDenseDecls<Phase::Check>/256 878µs ± 2% 800µs ± 1% -8.91% (p=0.000 n=19+20) BM_CompileAPIFileDenseDecls<Phase::Check>/1024 1.88ms ± 2% 1.72ms ± 1% -8.56% (p=0.000 n=19+20) BM_CompileAPIFileDenseDecls<Phase::Check>/4096 5.78ms ± 2% 5.28ms ± 1% -8.70% (p=0.000 n=20+18) BM_CompileAPIFileDenseDecls<Phase::Check>/16384 21.9ms ± 1% 20.1ms ± 1% -8.02% (p=0.000 n=18+20) BM_CompileAPIFileDenseDecls<Phase::Check>/65536 90.4ms ± 2% 83.1ms ± 1% -8.04% (p=0.000 n=19+20) BM_CompileAPIFileDenseDecls<Phase::Check>/262144 381ms ± 2% 352ms ± 1% -7.79% (p=0.000 n=19+19) ``` --------- Co-authored-by: Richard Smith <richard@metafoo.co.uk> Co-authored-by: josh11b <15258583+josh11b@users.noreply.github.com>
14 KiB
Idioms
Table of contents
- Overview
- C++ dialect
- Abbreviations used in the code (AKA Carbon abbreviation decoder ring)
.deffiles- Index types
- ValueStore
- Template metaprogramming
- Local lambdas to reduce duplicate code
- Immediately invoked function expressions (IIFE)
- Declarations in conditions
- CRTP or "Curiously recurring template pattern"
- Multiple inheritance
- Defining constants usable in constexpr contexts
Overview
The toolchain implementation uses some implementation techniques that may not be commonly found in typical C++ code.
C++ dialect
The toolchain implementation does not use some C++ features, following Google's C++ style guide:
Abbreviations used in the code (AKA Carbon abbreviation decoder ring)
Note that abbreviations are typically only used in code, not comments (except when referring to an entity from the code).
- Addr: "address"
- Arg: "argument"
- Decl: "declaration"
- Expr: "expression"
- SubExpr: "subexpression"
- Float: "floating point"
- Init: "initialization"
- Inst: "instruction"
- Int: "integer"
- Loc: "location"
- Param: "parameter"
- Paren: "parenthesis"
- Ref: "reference"
- Deref: "dereference"
- Subst: "substitute"
Phrase abbreviations (where we have an abbreviation for a phrase, where we wouldn't perform all of the abbreviations of those words individually):
- InitRepr: "initializing representation"
- ObjectRepr: "object representation"
- SemIR: "semantics intermediate representation"
- ValueRepr: "value representation"
.def files
The Carbon toolchain uses a technique related to X-macros to generate code that operates over a collection of types, enumerators, or another similar list of names. This works as follows:
- A
.deffile is provided, that is intended to be repeatedly included by way of#include. - The user of the
.defdefines a macro, with a name and a form specified by the.deffile, for example#define CARBON_EACH_WIDGET(Name) Scope::Name,. - A
#includeof the.deffile expands toCARBON_EACH_WIDGET(Name1),CARBON_EACH_WIDGET(Name2), ... for each widget name, and then#undefs theCARBON_EACH_WIDGETmacro.
For example:
enum Widgets {
#define CARBON_EACH_WIDGET(Name) Name,
#include "widgets.def"
}
... would expand to an enumeration definition with one enumerator per widget name.
EnumBase types
Most .def files will have a corresponding EnumBase
child class (if widgets.def has X-macros, widgets.h and widgets.cpp has
the EnumBase child class). These work similarly to an enum class, with the
addition of a name() function and << stream operator support. Many also have
further utility functions for information related to the enum value.
In code, these types and values can be used directly in a switch. They will
convert to an internal actual enum class for the switch, and receive
corresponding compiler safety checks that all enum values are handled.
Index types
Carbon makes frequent use of
IndexBase and IdBase. The IndexBase and
IdBase types are small wrappers around int32_t to provide a measure of
type-checking when passing around indices to vector-like storage types. The only
difference is that IndexBase supports all comparison operators, whereas
IdBase only supports equality comparison.
Variable naming will often have _id at the end to indicate that it corresponds
to an IdBase. This may include the full type, as in operand_inst_id being an
InstId for an operand.
A block is an array of ids. These will be indicated with either a _block
suffix or pluralization (for example, param_refs pluralizing refs).
The ref concept in a name means that there is an underlying instruction block,
but only a subset of instructions are present in the refs block. For example,
function parameters have a sequence, and also have a refs block with one entry
per parameter. The refs block allows parameters to be counted and accessed
directly, rather than through vector iteration.
ValueStore
Many of Carbon's data types are stored in a
ValueStore or related type with similar
semantics (sem_ir has several such classes).
ValueStore links an indexing type to a value type with vector-like storage.
The indices typically use IdBase.
ValueStores APIs follow the shape of simple array access and mutation:
Addwhich takes a value and returns the index.Setwhich takes a value and index to modify.Gettakes an index and returns a reference to the value (possibly a constant reference).- Other vector-like functionality, including
sizeorReserve
ValueStores should be named after the type they contain. The index type used on
the value store should have a using ValueType... which indicates the stored
type. When taking a return of one of these functions, it's common to use auto
and rely on the name of the storage type to imply the returned type.
Some name mirroring examples are:
-
intsis aValueStore<IntId>, which has an index type ofIntIdand a value type ofllvm::APInt. -
functionsis aValueStore<SemIR::FunctionId>, which has an index type ofSemIR::FunctionIdand a value type ofSemIR::Function. -
stringsis aValueStore<StringId>, which has an index type ofStringId, but for copy-related reasons, usesllvm::StringReffor values.
A fairly complete list of ValueStore uses should be available on
checking's Context class.
Template metaprogramming
FIXME: show example patterns
- TypedInstArgsInfo from toolchain/sem_ir/inst.h
- templated using
- std::declval
- decltype
- static_assert
- if constexpr
- template specialization, for example
Inst::FromRaw<T>(maybe also type traits?)
Struct reflection
The toolchain uses a primitive form of struct reflection to operate generically
over the fields in a typed SemIR instruction. This is implemented in
common/struct_reflection.h, and the interface to the functionality is
StructReflection::AsTuple(your_struct), which converts the given struct into a
std::tuple containing the same fields in the same order.
Field detection
The presence of specific fields in a struct with a specified type is detected using the following idiom:
template <typename T, typename = FieldType T::*>
constexpr bool HasField = false;
template <typename T>
constexpr bool HasField<T, decltype(&T::field)> = true;
This is intended to check the same property as the following concept, which we can't use because we currently need to compile in C++17 mode:
template <typename T> concept HasField = requires (T x) {
{ x.field } -> std::same_as<FieldType>;
};
To detect a field with a specific name with a type derived from a specified base type, use this idiom:
// HasField<T> is true if T has a `U field` field,
// where `U` extends `BaseClass`.
template <typename T, bool Enabled = true>
inline constexpr bool HasField = false;
template <typename T>
inline constexpr bool HasField<
T, bool(std::is_base_of_v<BaseClass, decltype(T::field)>)> = true;
The equivalent concept is:
template <typename T> concept HasField = requires (T x) {
{ x.field } -> std::derived_from<BaseClass>;
};
Local lambdas to reduce duplicate code
Sometimes code that would be repeated in a function is factored into a local variable containing a lambda:
auto common_code = [&](AType param1, AnotherType param2) {
// code that would otherwise be repeated
...
}
if (something) {
common_code(...);
}
if (something_else) {
common_code(...)
}
Compared to defining a new function, this has the advantage of being able to be declared in context and access the local variables of the enclosing function.
Immediately invoked function expressions (IIFE)
Instead of creating a separate function with its own name that will be called once to produce the initial value for a variable, the function can be declared inline and then immediately called.
This can be used for complex initialization, as in:
// variable declaration
static const llvm::ArrayRef<std::byte> entropy_bytes =
// initializer starts with a lambda
[]() -> llvm::ArrayRef<std::byte> {
static llvm::SmallVector<std::byte> bytes;
// a bunch of code
// return the value to initialize the variable with
return bytes;
// finish defining the lambda, and then immediately invoke it
}();
It can also be used inside a CARBON_DCHECK to avoid computation that is only
needed in debug builds:
CARBON_DCHECK([&] {
// a bunch of code
// condition that will be tested by CARBON_DCHECK
return complicated && multiple_parts;
// finish defining the lambda, and then immediately invoke it
}(), "Complicated things went wrong");
See a description of this technique on wikipedia.
Declarations in conditions
The condition part of an if statement may contain a declaration with an
initializer followed by a semicolon (;) and then the proper boolean condition
expression, as in:
if (auto verify = tree.Verify(); !verify.ok()) {
The condition can be replaced by a declaration entirely, as in:
if (auto equals = context.ConsumeIf(Lex::TokenKind::Equal)) {
// Equivalent to:
if (auto equals = context.ConsumeIf(Lex::TokenKind::Equal); equals) {
or
if (auto literal = bound_inst.TryAs<SemIR::IntegerLiteral>()) {
// Equivalent to:
if (auto literal = bound_inst.TryAs<SemIR::IntegerLiteral>(); literal) {
This is a common way of handling a function that returns an optional value.
See https://en.cppreference.com/w/cpp/language/if
CRTP or "Curiously recurring template pattern"
Curiously Recurring Template Pattern - cppreference.com
Curiously recurring template pattern - Wikipedia
Examples:
template <typename DerivedT, ...>in enum_base.htemplate <typename DerivedT>in ostream.h
Multiple inheritance
We use multiple inheritance to support uses of CRTP.
Example:
struct NameScopeId : public IndexBase, public Printable<NameScopeId> {
Defining constants usable in constexpr contexts
To declare a constant usable at compile time in constexpr contexts as a static
class member, we use this pattern:
Declaration:
class Foo {
// ...
static const std::array<ElementType, ElementCount> MyTable;
static constexpr auto ComputeMyTable()
-> std::array<ElementType, ElementCount> { ... }
};
Definition:
constexpr std::array<ElementType, ElementCount>
Foo::MyTable = Foo::ComputeMyTable();
Note the const on the declaration does not match the constexpr on
definition, and that the definition is outside of the class body. This allows
the initializer to depend on the definition of the class.
Further note that this only works with static members of classes, not static variables in functions.
Due to a Clang bug, this technique does not work in a class template. The following pattern can be used instead:
template <typename T>
class Foo {
// ...
template <typename Self = Foo>
static constexpr auto MyValueImpl = Self();
static constexpr const Foo& MyValue = MyValueImpl<>;
// ...
};
The parameters of the variable template can be chosen to allow reuse of the same variable template for multiple static data members.
Examples:
NodeStack::IdKindTablein check/node_stack.hBuiltinKind::ValidCountin sem_ir/builtin_inst_kind.h
A global constant may use a single definition without a separate declaration:
static constexpr std::array<bool, 256> IsIdStartByteTable = [] {
std::array<bool, 256> table = {};
// ...
return table;
}();
Note this example is using an immediately invoked function expression to compute the initial value, which is common.
Examples: