Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How is every dynamically checked aspect of Rust implemented?

Rust employs dynamic checking methods to check many things. One such example is bounds checking of arrays.

Take this code for example,

fn test_dynamic_checking() -> i32 {
    let x = [1, 2, 3, 4];
    x[1]
}

The resulting LLVM IR is:

; Function Attrs: uwtable
define internal i32 @_ZN10dynamic_ck21test_dynamic_checking17hcef32a1e8c339e2aE() unnamed_addr #0 {
entry-block:
  %x = alloca [5 x i32]
  %0 = bitcast [5 x i32]* %x to i8*
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* %0, i8* bitcast ([5 x i32]* @const7091 to i8*), i64 20, i32 4, i1 false)
  %1 = getelementptr inbounds [5 x i32], [5 x i32]* %x, i32 0, i32 0
  %2 = call i1 @llvm.expect.i1(i1 false, i1 false)
  br i1 %2, label %cond, label %next

next:                                             ; preds = %entry-block
  %3 = getelementptr inbounds i32, i32* %1, i64 1
  %4 = load i32, i32* %3
  ret i32 %4

cond:                                             ; preds = %entry-block
  call void @_ZN4core9panicking18panic_bounds_check17hcc71f10000bd8e6fE({ %str_slice, i32 }* noalias readonly dereferenceable(24) @panic_bounds_check_loc7095, i64 1, i64 5)
  unreachable
}

A branch instruction is inserted to decide whether the index is out of bounds or not, which doesn't exist in clang-compiled LLVM IR.

Here are my questions:

  1. In what situations does Rust implement dynamic checking?
  2. How does Rust implement dynamic checking in different situations?
  3. Is there any way to turn off the dynamic checking?
like image 271
Qoros Avatar asked Nov 06 '16 20:11

Qoros


2 Answers

Conceptually, Rust performs array bound checking on each and every array access. However, the compiler is very good at optimizing away the checks when it can prove that it's safe to do so.

The LLVM intermediate output is misleading because it still undergoes optimizations by the LLVM's optimizing machinery before the machine assembly is generated. A better way to inspect assembly output is by generating the final assembly using an invocation such as rustc -O --emit asm --crate-type=lib. The assembly output for your function is just:

        push    rbp
        mov     rbp, rsp
        mov     eax, 2
        pop     rbp
        ret

Not only is there no bound checking in sight, there is no array to begin with, the compiler has optimized the entire function to a return 2i32! To force bound checking, the function needs to be written so that Rust cannot prove that it can be elided:

pub fn test_dynamic_checking(ind: usize) -> i32 () {
   let x = [1, 2, 3, 4];
   x[ind]
}

This results in a larger assembly, where the bound check is implemented as the following two instructions:

        cmp     rax, 3    ; compare index with 3
        ja      .LBB0_2   ; if greater, jump to panic code

That is as efficient as it gets. Turning off the bound check is rarely a good idea because it can easily cause the program to crash. It can be done, but explicitly and only within the confines of an unsafe block or function:

pub unsafe fn test_dynamic_checking(ind: usize) -> i32 () {
   let x = [1, 2, 3, 4];
   *x.get_unchecked(ind)
}

The generated assembly shows the error checking to be entirely omitted:

        push    rbp
        mov     rbp, rsp
        lea     rax, [rip + const3141]
        mov     eax, dword ptr [rax + 4*rdi]
        pop     rbp
        ret

const3141:
        .long   1
        .long   2
        .long   3
        .long   4
like image 197
user4815162342 Avatar answered Oct 18 '22 12:10

user4815162342


In what situations does Rust implement dynamic checking?

It's a bit of a cop out... but Rust, the language, does not implement any dynamic checking. The Rust libraries, however, starting with the core and std libraries do.

A library needs to use run-time checking whenever not doing so could lead to a memory safety issue. Examples include:

  • guaranteeing that an index is within bounds, such as when implementing Index
  • guaranteeing that no other reference to an object exists, such as when implementing RefCell
  • ...

In general, the Rust philosophy is to perform as many checks as possible at compile-time, but some checks need by delayed to run-time because they depend on dynamic behavior/values.

How does Rust implement dynamic checking in different situations?

As efficiently as possible.

When dynamic checking is required, the Rust code will be crafted to be as efficient as possible. This can be slightly complicated, as it involves trying to ease the work of the optimizer and conforming to patterns that the optimizer recognizes (or fixing it), but we have the chance that a few developers are obsessed with performance (@bluss, for example).

Not all checks may be elided, of course, and those that remain will typically be just a branch.

Is there any way to turn off the dynamic checking?

Whenever dynamic checking is necessary to guarantee code safety, it is not possible to turn in off in safe code.

In some situations, though, an unsafe block or function may allow to bypass the check (for example get_unchecked for indexing).

This is not recommended in general, and should be a last-resort behaviour:

  1. Most of the times, the run-time checks have little to no performance impacts; CPU prediction is awesome like that.
  2. Even if they have some impact, unless they sit it a very hot-loop, it may not be worth optimizing them.
  3. Finally, if it does NOT optimize, it is worth trying to understand why: maybe it's impossible or maybe the optimizer is not clever enough... in the latter case, reporting the issue is the best way to have someone digging into the optimizer and fixing it (if you cannot or are not willing to do it yourself).
like image 2
Matthieu M. Avatar answered Oct 18 '22 11:10

Matthieu M.