Summary
This RFC proposes support for specifying per-function options via attributes. This allows passes to query function attributes for configuration values that might otherwise be set globally via command-line options or pass parameters. This support does not require changes to existing infrastructure, or even much new functionality, as it simply uses string attributes. In this case, “support” is better understood as the establishment of conventions governing how these attributes should be treated. The goal of this RFC is to determine if the community supports a permissive stance towards adding these kinds of attributes and, if so, the precise form they should take.
This RFC is limited to LLVM - it does not explicitly consider frontend changes that may take advantage of these attributes.
Example:
A function could carry a "gvn.enable-pre" attribute (corresponding to the enable-pre option in the gvn pass) that overrides whether PRE is enabled for that specific function:
define void @hot_function() #0 {
; ...
}
attributes #0 = { "gvn.enable-pre"="1" }
Passes would check for these attributes when determining option values:
bool GVNPass::isPREEnabled(const Function &F) const {
if (GVNEnablePRE.getNumOccurrences() > 0)
return GVNEnablePRE;
if (F.hasFnAttribute("gvn.enable-pre"))
return F.getFnAttributeAsParsedInteger("gvn.enable-pre");
return Options.AllowPRE.value_or(GVNEnablePRE);
}
Motivation
LLVM provides many pass-specific options that influence optimization heuristics and enablement. These options can significantly enhance performance, but they are typically set globally. Often, what options are beneficial for one function may be detrimental for another. Though it is sometimes possible to bypass this limitation through build process changes, doing so can be very cumbersome and can interfere with interprocedural optimization.
Example Use Cases
1. Library and Runtime Optimization
High-performance libraries may contain functions with different characteristics, e.g. it is common for some GPU kernels to be memory-bound while others are compute-bound. Function-level options enable library authors and build systems to encode beneficial settings for important functions based on performance measurements or domain knowledge.
2. Autotuning and Profile-Guided Optimization
Beneficial options can be discovered through autotuning, profiling, or even inferred at runtime. When different functions in the same module appear to benefit from different settings, there is no clean way to apply those results. With function-level options, optimization decisions can be made per-function without extensive build system modifications or source changes.
Proposed Design
Attribute Syntax
Use string function attributes with pass-specific names:
attributes #0 = {
"gvn.enable-pre"="1"
"inline.threshold"="400"
}
Using a non-existent attribute is a no-op.
Naming Convention
Attributes follow a dot-separated naming convention: "${passname}.${option}", where ${passname} is the pass name as registered in the pass pipeline and ${option} is similar to the name as it appears in the corresponding cl::opt string. For example:
"gvn.enable-pre"— theenable-preoption in thegvnpass"inline.threshold"— thethresholdoption in the inline pass
This namespacing prevents collisions across passes and makes it clear from the IR which pass an attribute belongs to.
It is not necessary for names to correspond exactly to cl::opt strings, but they should be named in such a way as to make it obvious which options they correspond to.
Pass Integration
Passes that check multiple sources of configuration preferences would use function attributes along with cl::opt, pass parameters, or TTI. Existing APIs can be used to query function attributes:
// Existing Function APIs
bool hasFnAttribute(StringRef Kind) const;
uint64_t getFnAttributeAsParsedInteger(StringRef Kind,
uint64_t Default = 0) const;
Together, these are sufficient to implement the proposed precedence order (see the introductory example). We are also happy to extend the Function API with additional helpers as needed, such as parsers for additional value types (bool, double, etc.) or std::optional-returning variants that make precedence checking more ergonomic.
Passes are free to use debug statements or optimization remarks to communicate about which options were applied. This could be especially useful as inlining may lead to dropped attributes.
Inlining Behavior
When a function is inlined, attributes are by default dropped rather than propagated or merged to the caller. This may be a concern since hot functions are often inlining candidates. It is possible to define merge rules, but it’s not clear if that would be desirable for what are essentially optimization hints. This also means that inlining order can affect behavior, for example in call graph SCC where the functions have different attributes. This RFC does not propose any explicit method of dealing with such limitations. Individual passes may communicate (un)successful application of these attributes as desired. Discussion on how this affects the viability of the proposal is welcome.
Precedence
Current LLVM passes are inconsistent here - some have cl::opt override pass arguments, others the reverse. In either case, attributes should slot in above pass arguments in priority so they will override pass arguments. The following is a proposed default precedence order (lowest to highest priority):
- Option defaults
- Target-specific defaults (TTI)
- Pass arguments
- Function attributes
- Command-line options (
cl::opt)
This allows function attributes to override pass defaults while still permitting global command-line overrides for debugging or testing. Note that command-line options only take precedence when explicitly set by the user. If a cl::opt retains its default value, function attributes take priority. Passes can distinguish these cases using cl::opt::getNumOccurrences().
This RFC does not propose that we align all passes currently using different precedence orders to use the same - though that may be worthwhile.
Stability Guarantees
These attributes are optimization hints with no cross-version stability guarantees. Passes may be modified or options dropped and any attributes referencing them will be silently ignored. There is no auto-upgrade path. This is consistent with the behavior of existing string attributes, which are a no-op when unrecognized.
LTO and ThinLTO
Function attributes are serialized in bitcode and preserved during LTO linking and ThinLTO function importing. No special handling is required.
Relationship with Loop Metadata
LLVM supports per-loop optimization hints via llvm.loop metadata (e.g., llvm.loop.unroll.count, llvm.loop.vectorize.width). This RFC does not propose changes to loop metadata.
Function attributes and loop metadata differ in what they can express:
- Loop metadata attaches to latch branch instruction and can specify per-loop settings.
- Function attributes attach to functions and can specify function-wide settings.
Some clients may find it easier to annotate functions than individual loops (where an equivalent option exists). Some passes may benefit from checking both mechanisms where applicable - for example, loop unrolling currently checks llvm.loop.unroll.count and the unroll-count CL option.
Why Attributes Over Metadata
This feature could be feasibly implemented using either function attributes or metadata. There is precedent for both as means of passing optimization hints, for example the inlinehint attribute and llvm.loop metadata. We propose function attributes for two reasons:
-
API convenience: Function attributes have convenient query utilities (
getFnAttributeAsParsedInteger) that could be straightforwardly expanded for more types. There are similar utilities for loop metadata (getBooleanLoopAttribute), but these would need adaptation for functions and new types. This is ultimately a minor difference. -
Preservation guarantees: Attributes have more predictable preservation semantics. Metadata can be more freely dropped by passes. Though again, these new attributes are not required for correctness, so dropping would not seem like a big deal and this is therefore another minor difference.
Anticipated Costs
-
Per-pass integration effort: For any attribute we wish to support, we must modify the relevant pass to check it. This can be done incrementally as desired by pass maintainers or users. We do not expect this to significantly increase maintenance needs as the new checks can occur in existing option-resolution functions. If those functions do not exist, we can add them. What is important is that new attributes are not new options - they are just one more source from which to determine the value of an option.
-
Attribute documentation: We should document supported attributes.
-
Testing: Each supported attribute should have test coverage for the attribute path.
Anticipated Benefits
-
Enables per-function tuning: Allows use cases that are currently impossible without source modifications or build system complexity.
-
Client flexibility: Frontends or other clients can attach these attributes however they choose - pragmas, command-line mapping, profile feedback, autotuning results, etc.
Other Questions
- Should any effort be made to align ways of handling existing options across passes?
- Are there particular options/passes that you would like to see supported?
And more broadly - are you interested in having this capability in LLVM?
Thanks!