Hello! In our last lesson, we explored LLVM's metadata system, focusing on how it attaches non-semantic, "value-add" information like debug locations to the IR. We saw that optimizers are free to discard metadata because it doesn't change what the program does.
Today, we shift our focus to another special type of function call you'll see in LLVM IR: intrinsic functions. While they might look similar to regular function calls, they are fundamentally different from both normal functions and metadata. This lesson fulfills the learning outcome: Describe the purpose of intrinsic functions and identify common categories. We will define what intrinsics are, why they are essential for a modern compiler, and survey the most common types you'll encounter. Understanding intrinsics is critical because they are the primary mechanism through which LLVM exposes advanced hardware capabilities and communicates high-level information from the frontend to the optimizer, both of which are central to your interest in performance systems.
What is an Intrinsic Function?
At its core, an LLVM intrinsic is a "function" whose semantics are built directly into the compiler. When the optimizer or backend encounters a call to a function starting with the llvm. prefix, it doesn't look for a C++ function body to inline or a library to link against. Instead, it triggers special, pre-defined logic within LLVM itself.
[PDF] Intrinsics, Metadata, and Attributes - LLVM
These presentation slides from a past LLVM Developers' Meeting provide a concise and authoritative overview. We'll start with the background definition.
Please look at the slide titled Background: Intrinsics. It gives the core definition and shows an example of llvm.memcpy. This is the fundamental concept we'll build on.
As you saw in the slide, intrinsics are "internal" functions known to LLVM. Like any other function, they must be declared before they are used, which you'll see as a declare statement in the IR, typically near the top of the module.
A Gentle Introduction to LLVM IR - mcyoung
This blog post provides another clear explanation with a code example.
Read the short section titled Intrinsics. This reinforces the declare syntax and the llvm. naming convention. Note the mangled name, p0.p0.i64, which encodes type information into the function name itself—a common pattern for intrinsics.
Unlike metadata, intrinsics have a profound impact on the program's semantics. An llvm.memcpy call performs a memory copy, and an llvm.sqrt call calculates a square root. The optimizer cannot simply discard them, although it can (and often does) transform, replace, or optimize them based on their known behavior.
The Purpose of Intrinsics
If they are just functions with special names, why does LLVM need them? Why not just use standard library calls or regular IR instructions? Intrinsics solve three main problems that are difficult to handle otherwise.
1. Operations Lacking a Single Instruction
Some high-level operations, like copying a block of memory (memcpy), don't map to a single instruction on most processors. While you could implement this as a library function, treating it as an intrinsic gives the optimizer superpowers.
When the optimizer sees call void @memcpy(...), the function is a black box. The optimizer knows nothing about it other than its signature. But when it sees call void @llvm.memcpy.p0.p0.i64(...), it knows:
- The first argument is the destination.
- The second argument is the source.
- The third argument is the number of bytes to copy.
- It doesn't read or write any other memory (it is
argmemonly). - It doesn't have side effects beyond the copy.
With this knowledge, the InstCombine pass can perform powerful optimizations. If the size is small and known, it can replace the intrinsic call with a sequence of discrete load and store instructions. If the source and destination pointers are the same, it can delete the call entirely. These optimizations are impossible with an opaque library call.
2. Exposing Target-Specific Functionality
This is where intrinsics become vital for high-performance computing. Modern CPUs have a vast array of specialized instructions for SIMD (Single Instruction, Multiple Data) operations, cryptography, bit manipulation, and more. These features have no direct representation in standard C++.
Intrinsics act as a bridge. A frontend can emit a generic llvm.masked.load intrinsic to represent a vectorized load that skips memory accesses based on a mask.

When this IR reaches the backend, the Instruction Selector (as shown in the diagram above) translates this generic intrinsic into the optimal instruction for the target architecture.
- On a CPU with AVX-512 support, it might become a single
vpgatherddinstruction. - On a CPU with ARM NEON, it might become a different set of instructions.
- On a CPU with no vector support, the backend might "lower" the intrinsic into a loop of scalar loads and conditional branches.
This allows frontends to target high-performance features in a portable way, leaving the target-specific details to the backend.
3. Communicating High-Level Knowledge to the Optimizer
Some intrinsics exist purely to pass information to the optimizer that is difficult to express otherwise.
A prime example from a language like Rust is checked arithmetic. In debug mode, Rust panics on integer overflow. This is typically implemented using intrinsics.
A Gentle Introduction to LLVM IR - mcyoung
The "Reading Some Codegen" section of the blog post has a fantastic, real-world example of this.
Read the section Reading Some Codegen. Observe how the Rust compiler uses two key intrinsics: @llvm.smul.with.overflow.i32: This performs a multiplication and returns a boolean flag indicating if overflow occurred. The backend can often map this to a very efficient instruction pair. @llvm.expect.i1: This tells the optimizer that the programmer expects the overflow flag to be false. The optimizer uses this hint to arrange the machine code so that the non-panicking path is the "hot" path, improving instruction cache and branch prediction performance.
Another powerful optimization intrinsic is @llvm.assume. It allows the frontend to assert a condition that the optimizer can then take for granted. For example, if you know a pointer is aligned to a 64-byte boundary, you can use __builtin_assume_aligned in C++, which Clang lowers to an @llvm.assume call. This knowledge allows the backend to generate more efficient vectorized memory accesses that require such alignment.
Common Categories of Intrinsics
While there are thousands of intrinsics, they generally fall into several well-defined categories.
- Memory Operations: The ones we've seen:
llvm.memcpy,llvm.memset,llvm.memmove. These are fundamental. - Arithmetic with Overflow:
llvm.sadd.with.overflow,llvm.umul.with.overflow, etc. They return a struct containing the result and an overflow bit. - Bit Manipulation:
llvm.bitreverse,llvm.ctpop(count population/set bits),llvm.bswap(byte swap). These map directly to common hardware instructions. - Debugger Support: We've seen these before!
llvm.dbg.declareandllvm.dbg.valueare intrinsics used to attach debug information. - Exception Handling: Intrinsics used to implement the landing pads for C++ exceptions.
- Optimizer Hints:
llvm.assume,llvm.expect,llvm.prefetch. These provide information to the optimizer without changing the program's explicit logic. - Vector/SIMD Operations: A massive category including element-wise arithmetic, shuffles, and more advanced operations like masked memory access.
[PDF] Intrinsics, Metadata, and Attributes - LLVM
Let's return to the presentation slides to see more examples, particularly performance-oriented ones.
First, glance at the table on the slide Some new things to get a feel for the variety. Next, read the two slides on Masked Memory Access. This shows the intrinsics for masked loads, stores, scatters, and gathers—all crucial for modern HPC and vectorization. Finally, read the slides on the @llvm.assume intrinsic. This explains its power and how it's exposed in Clang via __builtin_assume and alignment attributes.
- Target-Specific Intrinsics: While many intrinsics are target-independent, some are not. These are prefixed with the target name, like
llvm.x86.avx. ...orllvm.arm.neon. .... They are used when a hardware feature is so unique that it doesn't fit a generic abstraction.

This table shows a list of intrinsics developed for the CHERI research architecture. Each one maps directly to a custom instruction on the CHERI-MIPS processor, giving the compiler a way to generate code for this unique hardware.
Conclusion
In this lesson, we've established the crucial role of intrinsic functions in the LLVM ecosystem. They are far more than just specially-named functions; they are the mechanism for expressing complex operations, exposing hardware-specific features, and conveying high-level semantic information to the optimization and code generation passes.
Key Takeaways:
- Definition: Intrinsics are functions with names starting in
llvm.whose semantics are built into the compiler. - Purpose: They are used for operations that don't map to a single instruction, for exposing target-specific hardware, and for passing hints to the optimizer.
- Contrast with Metadata: Unlike metadata, intrinsics have defined semantic effects and cannot be safely discarded by optimizers.
- Key Categories: You'll frequently encounter intrinsics for memory operations, overflow-checked arithmetic, vector/SIMD processing, and optimizer hints like
llvm.assume. - Handling: Intrinsics are created by the frontend, consumed and transformed by the middle-end optimizers, and finally lowered to machine code by the backend.
We have now covered the essential components of LLVM IR: basic instructions, control flow, data types, metadata, and intrinsics. You have the foundational knowledge to read and understand most of the IR you will encounter. In our next lesson, we will put this knowledge to practical use. You will learn to use the clang tool to emit LLVM IR from C++ code and the llvm-dis/llvm-as tools to work with it, allowing you to see exactly how these concepts manifest in a real compilation pipeline.