看看所谓的 Zero cost abstraction

1,331 阅读6分钟
原文链接: zhuanlan.zhihu.com

我选了三个语言:Rust、C++、C# 来实现这么个功能:将 [0, 10) 这里面的数字乘 2 然后输出。先来 C# 吧:

using System;
using static System.Linq.Enumerable;

class Program
{
    static void Main(string[] args)
    {
        foreach (var x in Range(0, 10).Select(x => x * 2))
        {
            Console.WriteLine(x);
        }
    }
}

编译的配置:

<TargetFramework>netcoreapp1.1</TargetFramework>
<RuntimeIdentifiers>win10-x64</RuntimeIdentifiers>

我试图通过断点来直接看 X86 ASM 来看看 JIT 生成的代码,后来发现只能看见一堆奇怪的 call,想看 call 的什么玩意还得切换到 native 的 call stack 搜。搞来搞去我也没什么头绪,后来直接搞到了 RyuJIT 的 dump:

[FAILED: unprofitable inline] System.Linq.Enumerable:Range(int,int):ref

[FAILED: noinline per IL/cached result] System.MulticastDelegate:CtorClosed(ref,long):this

[FAILED: target not direct] System.Collections.Generic.IEnumerable`1[Int32][System.Int32]:GetEnumerator():ref:this

[FAILED: too many il bytes] System.Linq.Enumerable:Select(ref,ref):ref

[FAILED: target not direct] System.Collections.Generic.IEnumerator`1[Int32][System.Int32]:get_Current():int:this

[FAILED: noinline per IL/cached result] System.Console:WriteLine(int)

[FAILED: target not direct] System.Collections.IEnumerator:MoveNext():bool:this

[FAILED: target not direct] System.IDisposable:Dispose():this

看这意思就是,几乎一个都没能内联,和把 foreach 代码转换一下之后的样子差不多。而且这中间还夹杂着各种 virtual function 啊等。嗯,目测 overhead 很大。我相信很多语言的实现和 C# 这种差不多。

现在让我们看看 C++:

#include <range/v3/view.hpp>
#include <range/v3/algorithm.hpp>
#include <iostream>

int main()
{
    using namespace ranges::view;
    ranges::copy(
        iota(0, 10) | transform([](int x) { return x * 2; }),
        ranges::ostream_iterator<>{std::cout}
    );
}

用 g++ 6.3.1,-O2 编译:

	xorl	%ebx, %ebx
	movq	.refptr._ZSt4cout(%rip), %rsi
.L5:
	movl	%ebx, %edx
	movq	%rsi, %rcx
	call	_ZNSolsEi
	addl	$2, %ebx
	cmpl	$20, %ebx
	jne	.L5

直接就给优化成普通循环输出了,是否感觉很牛逼?作为衬托,我展示一下某垃圾编译器的编译结果(Microsoft (R) C/C++ Optimizing Compiler Version 19.10.24728 for x64,/Ox /EHsc /Ob2):

00007FF7E2CB1003  sub         rsp,0A8h  
00007FF7E2CB100A  mov         qword ptr [rsp+20h],0FFFFFFFFFFFFFFFEh  
    using namespace ranges::view;
    ranges::copy(
00007FF7E2CB1013  mov         rax,qword ptr [__imp_std::cout (07FF7E2CB30B8h)]  
00007FF7E2CB101A  mov         qword ptr [r11-78h],rax  
00007FF7E2CB101E  xor         ecx,ecx  
00007FF7E2CB1020  mov         qword ptr [r11-70h],rcx  
00007FF7E2CB1024  movaps      xmm0,xmmword ptr [rsp+30h]  
00007FF7E2CB1029  movdqa      xmmword ptr [rsp+40h],xmm0  
00007FF7E2CB102F  movzx       eax,byte ptr [rsp+0B0h]  
00007FF7E2CB1037  mov         byte ptr [r11+8],al  
00007FF7E2CB103B  movzx       eax,byte ptr [rsp+0B0h]  
00007FF7E2CB1043  mov         byte ptr [r11+9],al  
00007FF7E2CB1047  mov         dword ptr [rsp+30h],ecx  
00007FF7E2CB104B  mov         qword ptr [r11-70h],0Ah  
00007FF7E2CB1053  lea         r8,[r11+8]  
00007FF7E2CB1057  lea         rdx,[r11-78h]  
00007FF7E2CB105B  lea         rcx,[r11-58h]  
00007FF7E2CB105F  call        ranges::v3::operator|<ranges::v3::detail::take_exactly_view_<ranges::v3::iota_view<int,void>,1>,ranges::v3::view::view<ranges::v3::detail::pipeable_binder<std::_Binder<std::_Unforced,ranges::v3::view::transform_fn & __ptr64,std::_Ph<1> const & __ptr64,<lambda_75e7fbe4ee98d512233b9df1e638ba8e> > > >,42,0> (07FF7E2CB10F0h)  
00007FF7E2CB1064  nop  
00007FF7E2CB1065  lea         r9,[rsp+40h]  
00007FF7E2CB106A  mov         r8,rax  
00007FF7E2CB106D  lea         rdx,[rsp+70h]  
00007FF7E2CB1072  call        ranges::v3::copy_fn::operator()<ranges::v3::transform_view<ranges::v3::detail::take_exactly_view_<ranges::v3::iota_view<int,void>,1>,<lambda_75e7fbe4ee98d512233b9df1e638ba8e> >,ranges::v3::ostream_iterator<void,char,std::char_traits<char> >,ranges::v3::basic_iterator<ranges::v3::adaptor_cursor_detail::adaptor_cursor<ranges::v3::basic_iterator<ranges::v3::iota_view<int,void>,ranges::v3::default_sentinel>,ranges::v3::iter_transform_view<ranges::v3::detail::take_exactly_view_<ranges::v3::iota_view<int,void>,1>,ranges::v3::indirected<<lambda_75e7fbe4ee98d512233b9df1e638ba8e> > >::adaptor<1> >,ranges::v3::adaptor_cursor_detail::adaptor_cursor<ranges::v3::basic_iterator<ranges::v3::iota_view<int,void>,ranges::v3::default_sentinel>,ranges::v3::iter_transform_view<ranges::v3::detail::take_exactly_view_<ranges::v3::iota_view<int,void>,1>,ranges::v3::indirected<<lambda_75e7fbe4ee98d512233b9df1e638ba8e> > >::adaptor<1> > >,42,0> (07FF7E2CB1110h)  
00007FF7E2CB1077  nop  
00007FF7E2CB1078  mov         rcx,qword ptr [rsp+60h]  
00007FF7E2CB107D  cmp         rcx,0FFFFFFFFFFFFFFFFh  
00007FF7E2CB1081  je          main+0A6h (07FF7E2CB10A6h)  
00007FF7E2CB1083  movzx       eax,byte ptr [rsp+0B0h]  
00007FF7E2CB108B  mov         byte ptr [rsp+0B0h],al  
00007FF7E2CB1092  test        rcx,rcx  
00007FF7E2CB1095  je          main+0A6h (07FF7E2CB10A6h)  
00007FF7E2CB1097  lea         rax,[rcx-1]  
00007FF7E2CB109B  test        rax,rax  
00007FF7E2CB109E  je          main+0A6h (07FF7E2CB10A6h)  
00007FF7E2CB10A0  call        ranges::v3::detail::variant_data<>::apply<ranges::v3::detail::apply_visitor<ranges::v3::detail::ignore2nd<ranges::v3::detail::delete_fun>,std::nullptr_t>,2> (07FF7E2CB1520h)

再看看 Rust:

fn main() {
  for x in (0..10).map(|x| x * 2) {
      println!("{}", x);
  }
}

生成的代码:

movl	$0, 44(%rsp)
	leaq	44(%rsp), %r14
	movq	%r14, 48(%rsp)
	leaq	_ZN4core3fmt3num52_$LT$impl$u20$core..fmt..Display$u20$for$u20$i32$GT$3fmt17h4a1dcfe5d526a368E(%rip), %r15
	movq	%r15, 56(%rsp)
	leaq	ref.2(%rip), %rdi
	movq	%rdi, 64(%rsp)
	movq	$2, 72(%rsp)
	movq	$0, 80(%rsp)
	leaq	48(%rsp), %rbx
	movq	%rbx, 96(%rsp)
	movq	$1, 104(%rsp)
	leaq	64(%rsp), %rsi
	movq	%rsi, %rcx
	callq	_ZN3std2io5stdio6_print17haef5796acb795b44E
	movl	$2, 44(%rsp)
	movq	%r14, 48(%rsp)
	movq	%r15, 56(%rsp)
	movq	%rdi, 64(%rsp)
	movq	$2, 72(%rsp)
	movq	$0, 80(%rsp)
	movq	%rbx, 96(%rsp)
	movq	$1, 104(%rsp)
	movq	%rsi, %rcx
	callq	_ZN3std2io5stdio6_print17haef5796acb795b44E
	movl	$4, 44(%rsp)
	movq	%r14, 48(%rsp)
	movq	%r15, 56(%rsp)
	movq	%rdi, 64(%rsp)
	movq	$2, 72(%rsp)
	movq	$0, 80(%rsp)
	movq	%rbx, 96(%rsp)
	movq	$1, 104(%rsp)
	movq	%rsi, %rcx
	callq	_ZN3std2io5stdio6_print17haef5796acb795b44E
        '...

如果你实际编译一下的话会看到,Rustc 直接会将 0 - 18 全部展开,变成类似:

println!("{}", 0);
println!("{}", 2);
println!("{}", 4);
//...
println!("{}", 18);

那么有些同学可能就问了,你输出归输出,在两次 call print 之间的

        movl	$4, 44(%rsp)
	movq	%r14, 48(%rsp)
	movq	%r15, 56(%rsp)
	movq	%rdi, 64(%rsp)
	movq	$2, 72(%rsp)
	movq	$0, 80(%rsp)
	movq	%rbx, 96(%rsp)
	movq	$1, 104(%rsp)
	movq	%rsi, %rcx

是什么玩意?其实 Rust 遇到 println! 展开后会出现 format!,format! 会在编译时解析你的格式化字符串,并且根据解析的结果构造出一个 Argument,print 的时候直接传这个 Argument 的 struct 进去就可以。我们简单看一下这个 Argument:

#[derive(Copy)]
#[allow(missing_debug_implementations)]
#[unstable(feature = "fmt_internals", reason = "internal to format_args!",
           issue = "0")]
#[doc(hidden)]
pub struct ArgumentV1<'a> {
    value: &'a Void,
    formatter: fn(&Void, &mut Formatter) -> Result,
}

#[stable(feature = "rust1", since = "1.0.0")]
#[derive(Copy, Clone)]
pub struct Arguments<'a> {
    // Format string pieces to print.
    pieces: &'a [&'a str],

    // Placeholder specs, or `None` if all specs are default (as in "{}{}").
    fmt: Option<&'a [rt::v1::Argument]>,

    // Dynamic arguments for interpolation, to be interleaved with string
    // pieces. (Every argument is preceded by a string piece.)
    args: &'a [ArgumentV1<'a>],
}

我们就拿 args 举例吧,在 Rust 里面 &[T] 实际上有俩成员,一个是长度,一个是指针(参考 std::basic_string_view),然后我们再看

movq	$1, 104(%rsp)

这个是不是就是长度 1 呢?再看看

        leaq	_ZN4core3fmt3num52_$LT$impl$u20$core..fmt..Display$u20$for$u20$i32$GT$3fmt17h4a1dcfe5d526a368E(%rip), %r15
	movq	%r15, 56(%rsp)

这,是不是在初始化 formatter!

        leaq	44(%rsp), %r14
	movq	%r14, 48(%rsp)

这个就是在初始化 value,

leaq	48(%rsp), %rbx

这个就是把上面构造好了的 ArgumentV1 的地址存在 rbx 里,然后

movq	%rbx, 96(%rsp)

就是在初始化上面提到的 &[T] 里面的指针成员啦。(另外我看了一下 Rustc 生成的 LLVM-IR,是在生成 IR 的时候就已经全部展开了)

撒花完结。