Jeremy Fitzhardinge 5f29f11a4d Add -Zannotate-moves for profiler visibility of move/copy operations
This implements a new unstable compiler flag `-Zannotate-moves` that makes
move and copy operations visible in profilers by creating synthetic debug
information. This is achieved with zero runtime cost by manipulating debug
info scopes to make moves/copies appear as calls to `compiler_move<T, SIZE>`
and `compiler_copy<T, SIZE>` marker functions in profiling tools.

This allows developers to identify expensive move/copy operations in their
code using standard profiling tools, without requiring specialized tooling
or runtime instrumentation.

The implementation works at codegen time. When processing MIR operands
(`Operand::Move` and `Operand::Copy`), the codegen creates an `OperandRef`
with an optional `move_annotation` field containing an `Instance` of the
appropriate profiling marker function. When storing the operand,
`store_with_annotation()` wraps the store operation in a synthetic debug
scope that makes it appear inlined from the marker.

Two marker functions (`compiler_move` and `compiler_copy`) are defined
in `library/core/src/profiling.rs`. These are never actually called -
they exist solely as debug info anchors.

Operations are only annotated if the type:
   - Meets the size threshold (default: 65 bytes, configurable via
     `-Zannotate-moves=SIZE`)
   - Has a non-scalar backend representation (scalars use registers,
     not memcpy)

This has a very small size impact on object file size. With the default
limit it's well under 0.1%, and even with a very small limit of 8 bytes
it's still ~1.5%. This could be enabled by default.
2025-11-06 15:39:45 -08:00

193 lines
8.1 KiB
Rust

//@ compile-flags: -Z annotate-moves=1 -Copt-level=0 -g
#![crate_type = "lib"]
// Test with large array (non-struct type, Copy)
type LargeArray = [u64; 20]; // 160 bytes
#[derive(Clone, Default)]
struct NonCopyU64(u64);
// Test with Copy implementation
#[derive(Copy)]
struct ExplicitCopy {
data: [u64; 20], // 160 bytes
}
impl Clone for ExplicitCopy {
// CHECK-LABEL: <integration::ExplicitCopy as core::clone::Clone>::clone
fn clone(&self) -> Self {
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#EXPLICIT_COPY_LOC:]]
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#EXPLICIT_RETURN_LOC:]]
Self { data: self.data }
}
}
// Test with hand-implemented Clone (non-Copy)
struct NonCopyStruct {
data: [u64; 20], // 160 bytes
}
impl Clone for NonCopyStruct {
// CHECK-LABEL: <integration::NonCopyStruct as core::clone::Clone>::clone
fn clone(&self) -> Self {
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#CLONE_COPY_LOC:]]
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#CLONE_RETURN_LOC:]]
NonCopyStruct { data: self.data }
}
}
// CHECK-LABEL: integration::test_pure_assignment_move
pub fn test_pure_assignment_move() {
let arr: LargeArray = [42; 20];
// Arrays are initialized with a loop
// CHECK-NOT: call void @llvm.memcpy{{.*}}, !dbg ![[#]]
let _moved = arr;
}
// CHECK-LABEL: integration::test_pure_assignment_copy
pub fn test_pure_assignment_copy() {
let s = ExplicitCopy { data: [42; 20] };
// Arrays are initialized with a loop
// CHECK-NOT: call void @llvm.memcpy{{.*}}, !dbg ![[#]]
let _copied = s;
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#ASSIGN_COPY2_LOC:]]
let _copied_2 = s;
}
#[derive(Default)]
struct InitializeStruct {
field1: String,
field2: String,
field3: String,
}
// CHECK-LABEL: integration::test_init_struct
pub fn test_init_struct() {
let mut s = InitializeStruct::default();
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#INIT_STRUCT_LOC:]]
s = InitializeStruct {
field1: String::from("Hello"),
field2: String::from("from"),
field3: String::from("Rust"),
};
}
// CHECK-LABEL: integration::test_tuple_of_scalars
pub fn test_tuple_of_scalars() {
// Tuple of scalars (even if large) may use scalar-pair repr, so may not be annotated
let t: (u64, u64, u64, u64) = (1, 2, 3, 4); // 32 bytes
// Copied with explicit stores
// CHECK-NOT: call void @llvm.memcpy{{.*}}, !dbg ![[#]]
let _moved = t;
}
// CHECK-LABEL: integration::test_tuple_of_structs
pub fn test_tuple_of_structs() {
let s1 = NonCopyStruct { data: [1; 20] };
let s2 = NonCopyStruct { data: [2; 20] };
let tuple = (s1, s2); // Large tuple containing structs (320 bytes)
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#TUPLE_MOVE_LOC:]]
let _moved = tuple;
}
// CHECK-LABEL: integration::test_tuple_mixed
pub fn test_tuple_mixed() {
let s = NonCopyStruct { data: [1; 20] };
let tuple = (42u64, s); // Mixed tuple (168 bytes: 8 for u64 + 160 for struct)
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#MIXED_TUPLE_LOC:]]
let _moved = tuple;
}
// CHECK-LABEL: integration::test_explicit_copy_assignment
pub fn test_explicit_copy_assignment() {
let c1 = ExplicitCopy { data: [1; 20] };
// Initialized with loop
// CHECK-NOT: call void @llvm.memcpy{{.*}}, !dbg ![[#]]
let c2 = c1;
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#COPY2_LOC:]]
let _c3 = c1; // Can still use c1 (it was copied)
let _ = c2;
}
// CHECK-LABEL: integration::test_array_move
pub fn test_array_move() {
let arr: [String; 20] = std::array::from_fn(|i| i.to_string());
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#ARRAY_MOVE_LOC:]]
let _moved = arr;
}
// CHECK-LABEL: integration::test_array_in_struct_field
pub fn test_array_in_struct_field() {
let s = NonCopyStruct { data: [1; 20] };
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#FIELD_MOVE_LOC:]]
let data = s.data; // Move array field out of struct
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#FIELD_MOVE2_LOC:]]
let _moved = data;
}
// CHECK-LABEL: integration::test_clone_noncopy
pub fn test_clone_noncopy() {
let s = NonCopyStruct { data: [1; 20] };
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#CALL_CLONE_NONCOPY_LOC:]]
let _cloned = s.clone(); // The copy happens inside the clone() impl above
}
// CHECK-LABEL: integration::test_clone_explicit_copy
pub fn test_clone_explicit_copy() {
let c = ExplicitCopy { data: [1; 20] };
// Derived Clone on Copy type - the copy happens inside the generated clone impl
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#CALL_CLONE_COPY_LOC:]]
let _cloned = c.clone();
}
// CHECK-LABEL: integration::test_copy_ref
pub fn test_copy_ref(x: &ExplicitCopy) {
// CHECK: call void @llvm.memcpy{{.*}}, !dbg ![[#LOCAL_COPY_LOC:]]
let _local = *x;
}
// CHECK-DAG: ![[#EXPLICIT_COPY_LOC]] = !DILocation({{.*}}scope: ![[#EXPLICIT_COPY_SCOPE:]]
// CHECK-DAG: ![[#EXPLICIT_COPY_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_copy<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#EXPLICIT_RETURN_LOC]] = !DILocation({{.*}}scope: ![[#EXPLICIT_RETURN_SCOPE:]]
// CHECK-DAG: ![[#EXPLICIT_RETURN_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#CLONE_COPY_LOC]] = !DILocation({{.*}}scope: ![[#CLONE_COPY_SCOPE:]]
// CHECK-DAG: ![[#CLONE_COPY_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_copy<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#CLONE_RETURN_LOC]] = !DILocation({{.*}}scope: ![[#CLONE_RETURN_SCOPE:]]
// CHECK-DAG: ![[#CLONE_RETURN_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#ASSIGN_COPY2_LOC]] = !DILocation({{.*}}scope: ![[#ASSIGN_COPY2_SCOPE:]]
// CHECK-DAG: ![[#ASSIGN_COPY2_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#INIT_STRUCT_LOC]] = !DILocation({{.*}}scope: ![[#INIT_STRUCT_SCOPE:]]
// CHECK-DAG: ![[#INIT_STRUCT_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<alloc::string::String,{{ *[0-9]+}}>"
// CHECK-DAG: ![[#TUPLE_MOVE_LOC]] = !DILocation({{.*}}scope: ![[#TUPLE_MOVE_SCOPE:]]
// CHECK-DAG: ![[#TUPLE_MOVE_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#MIXED_TUPLE_LOC]] = !DILocation({{.*}}scope: ![[#MIXED_TUPLE_SCOPE:]]
// CHECK-DAG: ![[#MIXED_TUPLE_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#COPY2_LOC]] = !DILocation({{.*}}scope: ![[#COPY2_SCOPE:]]
// CHECK-DAG: ![[#COPY2_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#ARRAY_MOVE_LOC]] = !DILocation({{.*}}scope: ![[#ARRAY_MOVE_SCOPE:]]
// CHECK-DAG: ![[#ARRAY_MOVE_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<{{(array\$<|\[)alloc::string::String[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#FIELD_MOVE_LOC]] = !DILocation({{.*}}scope: ![[#FIELD_MOVE_SCOPE:]]
// CHECK-DAG: ![[#FIELD_MOVE_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#FIELD_MOVE2_LOC]] = !DILocation({{.*}}scope: ![[#FIELD_MOVE2_SCOPE:]]
// CHECK-DAG: ![[#FIELD_MOVE2_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_copy<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#CALL_CLONE_NONCOPY_LOC]] = !DILocation({{.*}}scope: ![[#CALL_CLONE_NONCOPY_SCOPE:]]
// CHECK-DAG: ![[#CALL_CLONE_NONCOPY_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#CALL_CLONE_COPY_LOC]] = !DILocation({{.*}}scope: ![[#CALL_CLONE_COPY_SCOPE:]]
// CHECK-DAG: ![[#CALL_CLONE_COPY_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_move<{{(array\$<|\[)u64[,;].*}},{{ *[0-9]+}}>"
// CHECK-DAG: ![[#LOCAL_COPY_LOC]] = !DILocation({{.*}}scope: ![[#LOCAL_COPY_SCOPE:]]
// CHECK-DAG: ![[#LOCAL_COPY_SCOPE]] = {{(distinct )?}}!DISubprogram(name: "compiler_copy<integration::ExplicitCopy,{{ *[0-9]+}}>"