Macro for stating unsafe assumptions in Rust.

Related tags

Database assume
Overview

assume

A macro for stating unsafe assumptions in Rust.

Using this macro, one can supply assumptions to the compiler for use in optimization. These assumptions are checked in debug_assertion configurations, and are unchecked (but still present) otherwise.

This is an inherently unsafe operation. It lives in the space between regular assert! and pure unsafe accesses - it relies heavily on an optimizing compiler's ability to track unreachable paths to eliminate unnecessary asserts.

[dependencies]
assume = "0.3"

Examples

use assume::assume;

let v = vec![1, 2, 3];

// Some computed index that, per invariants, is always in bounds.
let i = get_index();

assume!(
    unsafe: i < v.len(),
    "index {} is beyond vec length",
    i,
);
let element = v[i];  // Bounds check optimized out per assumption.
use assume::assume;

let items: HashMap<u32, String> = populate_items();

// Some item that, per invariants, always exists.
let item_zero_opt: Option<&String> = items.get(&0);

assume!(
    unsafe: item_zero_opt.is_some(),
    "item zero missing from items map",
);
let item_zero = item_zero_opt.unwrap();  // Panic check optimized out per assumption.
use assume::assume;

enum Choices {
    This,
    That,
    Other,
}

// Some choice that, per invariants, is never Other.
let choice = get_choice();

match choice {
    Choices::This => { /* ... */ },
    Choices::That => { /* ... */ },
    Choices::Other => {
        // This case optimized out entirely, no panic emitted.
        assume!(
            unsafe: @unreachable,
            "choice was other",
        );
    },
}

Motivation

Often, programs have invariants that are not or cannot be expressed in the type system. Rust is safe by default, and asserts are made as needed to ensure this.

Consider the following (somewhat convoluted) example:

pub struct ValuesWithEvens {
    values: Vec<u32>,
    evens: Vec<usize>,
}

impl ValuesWithEvens {
    pub fn new(values: Vec<u32>) -> Self {
        let evens = values
            .iter()
            .enumerate()
            .filter_map(|(index, value)| if value % 2 == 0 { Some(index) } else { None })
            .collect();

        Self { values, evens }
    }

    pub fn pop_even(&mut self) -> Option<u32> {
        if let Some(index) = self.evens.pop() {
            // We know this index is valid, but a bounds check is performed anyway.
            let value = self.values[index];

            Some(value)
        } else {
            None
        }
    }
}

fn main() {
    let mut vwe = ValuesWithEvens::new(vec![1, 2, 3, 4]);

    let last_even = vwe.pop_even().unwrap();
    println!("{}", last_even);
}

By construction, indices contained within evens are always valid indices into values. However, as written there is a bounds check on the line:

let value = self.values[index];

This ensures a bug in the program does not result in an out of bounds access. (For example, if another method were introduced that modified values it could invalidate the indices - this would not result in undefined behavior thanks to bounds checking.)

However, if this is a hot-spot in the program we may want to remove this check. Sometimes this trade-off is necessary to achieve performance requirements. Rust offers unsafe access:

    pub fn pop_even(&mut self) -> Option<u32> {
        if let Some(index) = self.evens.pop() {
            let value = unsafe { *self.values.get_unchecked(index) };

            Some(value)
        } else {
            None
        }
    }

As expected this has no bounds check, but other than the unsafe keyword we've removed any scrutiny around the access. We can improve this by including a debug-only assertion that the index really is okay:

    pub fn pop_even(&mut self) -> Option<u32> {
        if let Some(index) = self.evens.pop() {
            debug_assert!(index < self.evens.len());
            let value = unsafe { *self.values.get_unchecked(index) };

            Some(value)
        } else {
            None
        }
    }

Can you spot the bug? We've asserted against the wrong vector! This should be:

debug_assert!(index < self.values.len());
//                         ^^^^^^

The decoupling of assertion to optimization is unwieldy and error-prone.

The assume! macro relies on the optimizer's ability to validate and use stated assumptions - an incorrect assumption will have no effect and the bounds check will remain in the program.

Using the assume! macro looks like:

    pub fn pop_even(&mut self) -> Option<u32> {
        if let Some(index) = self.evens.pop() {
            assume!(
                unsafe: index < self.evens.len(),
                "even index {} beyond values vec",
                index
            );
            let value = self.values[index];

            Some(value)
        } else {
            None
        }
    }

Now the optimizer is aware of what we believe to be true, and is checking that this expression implies the optimization we want. In this case it does, so the bounds check is removed. Furthermore, this will assert our condition holds in debug_assertion configurations (such as in tests).

Best of all, the code we actually want to write remains untouched and easy to read.

When not to use

Do not use this macro.

Rely on assert! to check program invariants.

Rely on unreachable! to state that some code path should never be taken.

When to use

Okay - once you:

  • Have a reliable method for measuring your performance.
  • Have profiling results indicating some invariant check is causing overhead.
  • Have no way of re-arranging the program to express this without overhead.
  • Are about to reach for an unsafe get operation.

Then you should consider assume! instead. This is more terse, leaves your safe code untouched, asserts in debug builds, and ensures runtime checks are removed only if they are implied by the assumption.

This is not a beginner-friendly macro; you are expected to be able to view disassembly and verify the desired optimizations are taking place. You are also expected to have a suite of tests that build with debug_assertion enabled in order to catch violations of the invariant.

Gotchas

  • Unlike debug_assert! et. al., the condition of an assume! is always present. Complicated assumptions involving function calls and side effects are unlikely to be unhelpful in any case, but be aware they will run (unless the compiler can prove it is not needed). The assumed expression ought to be trivial and involve only the immediately available facts to guarantee this.

  • As stated, this relies on the optimizer to propagate the asumption. Differences in optimization level or mood of the compiler may cause it to fail to elide assertions in the final output. You are expected to benchmark and analyze the output yourself. If you simply must have no checking and do not want to rely on optimizations, then a debug_assert! + unchecked access is the way to go.

  • Avoid using assume!(unsafe: false) to indicate unreachable code. Although this works, the return type is () and not !, so the unreachability is not expressed to the compiler. This can result in warnings, or errors if e.g. different branches are computing some specific value. Use assume!(unsafe: @unreachable) instead.

See Also

The underlying mechanism for the macro is std::hint::unreachable_unchecked.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

You might also like...
An Elasticsearch REST API client for Rust

elastic elastic is an efficient, modular API client for Elasticsearch written in Rust. The API is targeting the Elastic Stack 7.x. elastic provides st

An etcd client library for Rust.

etcd An etcd client library for Rust. etcd on crates.io Documentation for the latest crates.io release Running the tests Install Docker and Docker Com

etcd for rust

etcd for Rust An etcd(API v3) client for Rust, and it provides async/await APIs backed by tokio and tonic. Documentation on the library can be found a

Rust bindings for LMDB

lmdb-rs Rust bindings for LMDB Documentation (master branch) Building LMDB is bundled as submodule so update submodules first: git submodule update --

The official MongoDB Rust Driver

MongoDB Rust Driver This repository contains the officially supported MongoDB Rust driver, a client side library that can be used to interact with Mon

Redis library for rust

redis-rs Redis-rs is a high level redis library for Rust. It provides convenient access to all Redis functionality through a very flexible but low-lev

rust wrapper for rocksdb

rust-rocksdb Requirements Clang and LLVM Contributing Feedback and pull requests welcome! If a particular feature of RocksDB is important to you, plea

UnQLite wrapper 1.0 is avaliable for Rust

unqlite A high-level UnQLite database engine wrapper. NOTE: Some of the documents is stolen from UnQLite Official Website. What is UnQLite? UnQLite is

Pure Rust library for Apache ZooKeeper built on MIO

rust-zookeeper Zookeeper client written 100% in Rust This library is intended to be equivalent with the official (low-level) ZooKeeper client which sh

Comments
  • Prevent call site environment from affecting `assume!`'s behavior

    Prevent call site environment from affecting `assume!`'s behavior

    Paths generated by macros are resolved in the call site's context. This means unprefixed paths like cfg!can refer to whichever the item imported in the call site's environment, not the one defined by the standard library. This PR rewrites all such paths to fix the issue.

    /// Rogue macro
    macro_rules! cfg {
        ($($tt:tt)*) => {
            return
        };
    }
    
    /// Rogue "crate"
    mod core {}
    
    #[test]
    #[should_panic]
    #[cfg(debug_assertions)]
    fn should_not_affected_by_call_site_environment() {
        assume!(unsafe: @unreachable);
    }
    
    opened by yvt 1
Owner
Nicholas Gorski
Nicholas Gorski
🧰 The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, SQLite, and MSSQL.

SQLx ?? The Rust SQL Toolkit Install | Usage | Docs Built with ❤️ by The LaunchBadge team SQLx is an async, pure Rust† SQL crate featuring compile-tim

launchbadge 7.6k Dec 31, 2022
Redis re-implemented in Rust.

rsedis Redis re-implemented in Rust. Why? To learn Rust. Use Cases rsedis does not rely on UNIX-specific features. Windows users can run it as a repla

Sebastian Waisbrot 1.6k Jan 6, 2023
A generic connection pool for Rust

r2d2 A generic connection pool for Rust. Documentation Opening a new database connection every time one is needed is both inefficient and can lead to

Steven Fackler 1.2k Jan 8, 2023
An ArangoDB driver for Rust

Rincon Rincon is an ArangoDB driver for Rust. It enables low level access to ArangoDB in a typesafe and Rust idiomatic manner. The name Rincon is deri

Innoave 35 Mar 21, 2021
Cassandra DB native client written in Rust language. Find 1.x versions on https://github.com/AlexPikalov/cdrs/tree/v.1.x Looking for an async version? - Check WIP https://github.com/AlexPikalov/cdrs-async

CDRS CDRS is looking for maintainers CDRS is Apache Cassandra driver written in pure Rust. ?? Looking for an async version? async-std https://github.c

Alex Pikalov 338 Jan 1, 2023
Cassandra (CQL) driver for Rust, using the DataStax C/C++ driver under the covers.

cassandra-cpp This is a maintained Rust project that exposes the DataStax cpp driver at https://github.com/datastax/cpp-driver/ in a somewhat-sane cra

null 93 Jan 7, 2023
CouchDB client-side library for the Rust programming language

Chill Chill is a client-side CouchDB library for the Rust programming language, available on crates.io. It targets Rust Stable. Chill's three chief de

null 35 Jun 26, 2022
Sofa - CouchDB for Rust

Sofa - CouchDB for Rust Documentation Here: http://docs.rs/sofa Installation [dependencies] sofa = "0.6" Description This crate is an interface to Cou

66 Origin 40 Feb 11, 2022
⚡🦀 🧨 make your rust types fit DynamoDB and visa versa

?? ?? dynomite dynomite makes DynamoDB fit your types (and visa versa) Overview Goals ⚡ make writing dynamodb applications in rust a productive experi

Doug Tangren 197 Dec 15, 2022
A Rust client for the ElasticSearch REST API

rs-es Introduction An ElasticSearch client for Rust via the REST API. Targetting ElasticSearch 2.0 and higher. Other clients For later versions of Ela

Ben Ashford 218 Dec 27, 2022