September 6, 2022·4 min read·engineeringcppclangperformance

Clang 15 removes debug-build calls for standard utility operations

Clang 15 treats move, forward, and related utility functions as compiler builtins. The change removes unnecessary implementation work at O0 while preserving the operations expressed by their types.

A function whose entire purpose is a cast can still appear as a function call in an unoptimized build. That makes C++'s low-overhead abstractions more expensive to inspect and execute during development than their semantics require. The Clang 15 release notes address this for std::move, std::forward, std::move_if_noexcept, std::addressof, and std::as_const. Clang now treats these operations as builtins and implements them directly instead of instantiating their standard-library definitions.

The LLVM 15.0.0 release became available on September 6. Its stated benefit here is improved code generation at -O0. The release does not attach a universal benchmark improvement to the change, and a program's actual cost still depends on the operations it performs after these utility expressions.

I find C++'s zero-overhead claim broadly persuasive, with compilation time and diagnostics often taking much of the cost. This change concerns another practical part of that tradeoff. A development build should not need a runtime call solely to express a cast whose meaning the compiler already understands.

std::move does not perform the transfer

The libc++ 15 move header shows the operation directly. It removes a reference from the deduced type and returns a cast to an rvalue reference. The function does not itself allocate storage, destroy the source, or invoke a move constructor.

This distinction remains true when the compiler recognizes the operation as a builtin. A later constructor or assignment may use the resulting expression to select an overload that transfers resources. The utility expression and the selected operation are different parts of the program.

#include <utility>

struct token {};

void consume(const token&);
void consume(token&&);

void dispatch(token& value)
{
    consume(std::move(value));
}

The expression selects the overload accepting an rvalue reference. What that overload does with the token belongs to its implementation. No resource transfer is specified merely by placing std::move around value. Clang's builtin treatment removes the library-wrapper implementation step while retaining that expression-level role.

This is useful when interpreting performance reports. Removing a call to the utility function does not remove a subsequent allocation or copy performed by the selected overload. Nor does it establish that a class has an inexpensive move operation. The change concerns the standard utility wrapper the compiler recognizes.

Forwarding preserves the requested category

The libc++ forward header implements two overloads returning casts to T&&. One includes a static assertion preventing an rvalue from being forwarded as an lvalue through an incompatible template argument. The type parameter is therefore essential to what forwarding means.

In a wrapper with a forwarding-reference parameter, that parameter's deduced type carries information about the original argument. Naming the parameter inside the function does not automatically preserve its original expression category. std::forward<T> uses the type information to produce the expression the wrapper intends to pass onward.

Compiler recognition does not make arbitrary forwarding safe. A wrapper can still choose an incorrect type argument or pass a reference onward after its referent's lifetime ends. The utility's correctness is about expressing the intended cast and satisfying its type requirements. Ownership and lifetime remain properties of the surrounding program.

The release's implementation approach is nevertheless appropriate for an operation this small. The frontend already needs the relevant type and expression-category information to check the call. Handling the standard utility directly can avoid building runtime wrapper code that contributes no separate application behaviour.

Conditional movement remains a type policy

The move header also defines move_if_noexcept through a conditional reference type. If move construction may throw and copying is available, the selected type is a const lvalue reference. Otherwise it is an rvalue reference. This is a type-dependent policy intended to let subsequent operations choose an appropriate constructor.

Treating the function as a builtin does not mean the compiler discovers at runtime whether a particular move will throw. The condition uses construction traits associated with the type. The selected reference influences overload resolution in the surrounding expression.

That makes it another example where wrapper overhead and actual work differ. A copy selected by this policy may have substantial cost. Clang's change removes unnecessary implementation of the wrapper without changing why the copy or move alternative is selected.

I prefer optimizations that preserve the source's explicit intent at this level. Replacing a standard utility call with a handwritten cast throughout a codebase would make generic code harder to recognize. Compiler support lets the program retain the conventional spelling while avoiding its avoidable unoptimized implementation cost.

Const access does not copy the object

The libc++ as_const header returns a const lvalue reference to its argument. Its rvalue overload is deleted. The operation supplies const access to an existing object without constructing an independent const copy.

Clang 15 lists this operation among the directly implemented utilities. Again, the surrounding object still has its ordinary identity and lifetime. The builtin does not extend that lifetime or make other aliases const. It implements the access expression described by the function's type.

The header makes as_const available for language versions after C++14. That is a library declaration condition separate from Clang's builtin recognition. Using a newer compiler does not by itself make a name visible when the selected language mode and headers omit its declaration.

O0 is a meaningful implementation target

The release notes explicitly connect these changes to -O0, where an application should not assume the ordinary optimizing pipeline will remove every small abstraction. Their stated mechanism is compiler builtin handling instead of instantiation of the standard-library body. This makes the implementation strategy different from merely asking the optimizer to inline more functions.

That scope makes the claim assessable. The selected functions have small, recognizable semantics involving casts, address acquisition, or reference access. Implementing them directly can remove wrapper calls without requiring the compiler to optimize the entire surrounding algorithm. A debug build still executes the constructors, assignments, and application functions that those expressions select.