Repository: rust
Issue: Output code bloat when programming with a functional style
Labels: I-slow,C-enhancement,T-compiler,A-MIR,C-optimization

I have been wanting to experiment writing some code in Rust with a more “pure functional / Haskelly” style. To explain things more clearly, let’s say you have this type.

```rust
#[derive(Debug)]
struct Type {
    field1 : u64,
    field2 : u32,
    field3 : Vec<u32>,
}
```

A very simple method to modify `field2` would be written so in everyday Rust.

```rust
impl Type   {
    #[no_mangle]
    #[inline(never)]
    fn mut_update(&mut self, field2 : u32) -> &mut Self {
        self.field2 = field2;
        self
    }
}
```

The `&mut Self` as return value is here so that one can chain such method calls. Now here is a more “pure functional” way of doing it.

```rust
impl Type   {
    #[no_mangle]
    #[inline(never)]
    fn func_update(self, field2 : u32) -> Self  {
        Type { field2 : field2, .. self }
    }
}
```

And once optimized out, these two functions should be perfectly identical, because `func_update` takes the property of `self` and never returns it, so the memory can be safely reused.

So I tested it out with this basic code.

```rust
fn main()   {
    let mut var = Type { field1 : 42, field2 : 0, field3 : vec![12] };

    let _ = var.mut_update(79);
    let _other = var.func_update(112);
}
```

But no… This is the assembly generated for `mut_update`

```nasm
mut_update:
	.cfi_startproc
	mov	dword ptr [rdi + 32], 79
	ret
```

And this is the one generated for `func_update`.

```nasm
func_update:
	.cfi_startproc
	mov	rax, qword ptr [rsi]
	mov	qword ptr [rdi], rax
	mov	dword ptr [rdi + 32], 112
	mov	rax, qword ptr [rsi + 24]
	mov	qword ptr [rdi + 24], rax
	movups	xmm0, xmmword ptr [rsi + 8]
	movups	xmmword ptr [rdi + 8], xmm0
	mov	rax, rdi
	ret
```

As you can see, the whole memory occupied by `self` is copied to another location, a pointer to which is returned by the function. And here is the LLVM IR for these two functions.

```llvm
; Function Attrs: noinline norecurse nounwind uwtable
define internal fastcc void @mut_update(%Type* nocapture dereferenceable(40)) unnamed_addr #0 {
start:
  %1 = getelementptr inbounds %Type, %Type* %0, i64 0, i32 4
  store i32 79, i32* %1, align 4
  ret void
}

; Function Attrs: noinline nounwind uwtable
define internal fastcc void @func_update(%Type* noalias nocapture sret dereferenceable(40), %Type* noalias nocapture readonly dereferenceable(40)) unnamed_addr #1 {
start:
  %self.sroa.0.0..sroa_idx = getelementptr inbounds %Type, %Type* %1, i64 0, i32 0
  %self.sroa.0.0.copyload = load i64, i64* %self.sroa.0.0..sroa_idx, align 8
  %self.sroa.4.0..sroa_idx = getelementptr inbounds %Type, %Type* %1, i64 0, i32 2
  %self.sroa.4.0..sroa_cast2 = bitcast %"collections::vec::Vec<u32>"* %self.sroa.4.0..sroa_idx to i8*
  %2 = getelementptr inbounds %Type, %Type* %0, i64 0, i32 0
  store i64 %self.sroa.0.0.copyload, i64* %2, align 8
  %3 = getelementptr inbounds %Type, %Type* %0, i64 0, i32 4
  store i32 112, i32* %3, align 8
  %self.sroa.4.8..sroa_idx = getelementptr inbounds %Type, %Type* %0, i64 0, i32 2
  %self.sroa.4.8..sroa_cast = bitcast %"collections::vec::Vec<u32>"* %self.sroa.4.8..sroa_idx to i8*
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* %self.sroa.4.8..sroa_cast, i8* %self.sroa.4.0..sroa_cast2, i64 24, i32 8, i1 false)
  ret void
}
```

The “functional” variant is already bloated at this point, so the problem comes form rustc itself.

That’s the first problem. The other is in the `main` function: look at what happens between the call to the two functions.

```nasm
	mov	dword ptr [rax], 12
	mov	qword ptr [rsp + 8], 42
	mov	dword ptr [rsp + 40], 0
	mov	qword ptr [rsp + 16], rax
	mov	qword ptr [rsp + 24], 1
	mov	qword ptr [rsp + 32], 1
	lea	rdi, [rsp + 8]
	call	mut_update
	mov	rax, qword ptr [rsp + 40]
	mov	qword ptr [rsp + 80], rax
	movups	xmm0, xmmword ptr [rsp + 8]
	movups	xmm1, xmmword ptr [rsp + 24]
	movaps	xmmword ptr [rsp + 64], xmm1
	movaps	xmmword ptr [rsp + 48], xmm0
	lea	rdi, [rsp + 96]
	lea	rsi, [rsp + 48]
	call	func_update
```

Yes, you read it right: the whole memory space containing `var` is being copied to another place *before* the next function is even called. So it is copied twice: immediately before the `call` and immediately after.

Here is the LLVM IR for the `main` function.

```llvm
; Function Attrs: uwtable
define internal void @_ZN4opti4main17ha95f343356fbf6f4E() unnamed_addr #2 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* @rust_eh_personality {
start:
  %_9 = alloca %Type, align 8
  %_other = alloca %Type, align 8
  %var = alloca %Type, align 8
  %0 = bitcast %Type* %var to i8*
  call void @llvm.lifetime.start(i64 40, i8* nonnull %0)
  %1 = tail call i8* @__rust_allocate(i64 4, i64 4) #4
  %2 = icmp eq i8* %1, null
  br i1 %2, label %bb5.i, label %bb5

bb5.i:                                            ; preds = %start
  tail call void @_ZN5alloc3oom3oom17h5b02814f1abf9784E()
  unreachable

bb5:                                              ; preds = %start
  %3 = bitcast i8* %1 to i32*
  store i32 12, i32* %3, align 4
  %4 = getelementptr inbounds %Type, %Type* %var, i64 0, i32 0
  store i64 42, i64* %4, align 8
  %5 = getelementptr inbounds %Type, %Type* %var, i64 0, i32 4
  store i32 0, i32* %5, align 8
  %_2.sroa.0.0..sroa_idx = getelementptr inbounds %Type, %Type* %var, i64 0, i32 2, i32 0, i32 0, i32 0, i32 0
  %6 = bitcast i32** %_2.sroa.0.0..sroa_idx to i8**
  store i8* %1, i8** %6, align 8
  %_2.sroa.4.0..sroa_idx11 = getelementptr inbounds %Type, %Type* %var, i64 0, i32 2, i32 0, i32 2
  store i64 1, i64* %_2.sroa.4.0..sroa_idx11, align 8
  %_2.sroa.5.0..sroa_idx13 = getelementptr inbounds %Type, %Type* %var, i64 0, i32 2, i32 2
  store i64 1, i64* %_2.sroa.5.0..sroa_idx13, align 8
  call fastcc void @mut_update(%Type* nonnull dereferenceable(40) %var)
  %7 = bitcast %Type* %_other to i8*
  call void @llvm.lifetime.start(i64 40, i8* nonnull %7)
  %8 = bitcast %Type* %_9 to i8*
  call void @llvm.lifetime.start(i64 40, i8* nonnull %8)
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* nonnull %8, i8* nonnull %0, i64 40, i32 8, i1 false)
  call fastcc void @func_update(%Type* noalias nocapture nonnull sret dereferenceable(40) %_other, %Type* noalias nocapture nonnull dereferenceable(40) %_9)
  call void @llvm.lifetime.end(i64 40, i8* nonnull %8)
  %9 = getelementptr inbounds %Type, %Type* %_other, i64 0, i32 2, i32 0, i32 2
  %10 = load i64, i64* %9, align 8
  %not..i.i.i.i = icmp eq i64 %10, 0
  br i1 %not..i.i.i.i, label %bb6, label %bb6.i.i.i.i

bb6.i.i.i.i:                                      ; preds = %bb5
  %11 = getelementptr inbounds %Type, %Type* %_other, i64 0, i32 2
  %12 = shl i64 %10, 2
  %13 = bitcast %"collections::vec::Vec<u32>"* %11 to i8**
  %_3.sroa.0.0.copyload3.i1.i.i.i.i = load i8*, i8** %13, align 8, !alias.scope !1
  tail call void @__rust_deallocate(i8* %_3.sroa.0.0.copyload3.i1.i.i.i.i, i64 %12, i64 4) #4
  br label %bb6

bb6:                                              ; preds = %bb6.i.i.i.i, %bb5
  call void @llvm.lifetime.end(i64 40, i8* nonnull %7)
  call void @llvm.lifetime.end(i64 40, i8* nonnull %0)
  ret void
}
```

I know next to nothing about LLVM IR, so I don’t really understand what is going on in there. But I guess that this line is responsible for the unnecessary copy.

```llvm
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* nonnull %8, i8* nonnull %0, i64 40, i32 8, i1 false)
```

I hope you people find a solution to this, because it is a huge loss in processor time to do almost thrice as much memory access as would be needed. And even if it is not relevant on such small a code, with a bigger code base, it must surely bloat memory usage compared to C for instance.