Skip to content

Commit 93f01d0

Browse files
authored
Native libraries use mimalloc as global allocator (#1249)
This PR moves the global allocator to MS's [mimalloc](https://github.com/microsoft/mimalloc). The performance gains are significant, and it adds `98KB` to the Python wheel and `215KB` to the native dylib used in the python wheel. This dependency adds `2.5s` to the build time. We can investigate wasm usage in the future, but this would likely require having a wasi-libc installation available for the build process to give mimalloc a wasi compatible libc to link against. Benchmarks on Apple M1 Max 64GB ``` Teleport evaluation time: [44.024 µs 44.043 µs 44.063 µs] change: [-21.789% -21.595% -21.420%] (p = 0.00 < 0.05) Performance has improved. Deutsch-Jozsa evaluation time: [3.0302 ms 3.0336 ms 3.0375 ms] change: [-23.878% -23.671% -23.493%] (p = 0.00 < 0.05) Performance has improved. Large file parity evaluation time: [28.956 ms 28.972 ms 28.989 ms] change: [-1.9178% -1.8288% -1.7431%] (p = 0.00 < 0.05) Performance has improved. Array append evaluation time: [306.58 µs 306.78 µs 307.02 µs] change: [-14.312% -14.068% -13.881%] (p = 0.00 < 0.05) Performance has improved. Array update evaluation time: [304.71 µs 304.84 µs 305.00 µs] change: [-10.777% -10.651% -10.537%] (p = 0.00 < 0.05) Performance has improved. Array literal evaluation time: [144.03 µs 144.32 µs 144.68 µs] change: [-39.800% -39.455% -39.171%] (p = 0.00 < 0.05) Performance has improved. Large nested iteration time: [30.418 ms 30.446 ms 30.476 ms] change: [-11.368% -11.252% -11.131%] (p = 0.00 < 0.05) Performance has improved. Large input file time: [15.869 ms 15.883 ms 15.900 ms] change: [-30.391% -30.267% -30.147%] (p = 0.00 < 0.05) Performance has improved. Standard library time: [9.5701 ms 9.5801 ms 9.5919 ms] change: [-28.527% -28.371% -28.224%] (p = 0.00 < 0.05) Performance has improved. ```
1 parent 9c4a706 commit 93f01d0

File tree

23 files changed

+365
-3
lines changed

23 files changed

+365
-3
lines changed

.devcontainer/devcontainer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
2-
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
2+
// README at: https://github.com/devcontainers/templates/tree/main/src/cpp
33
{
44
"name": "qsharp",
5-
"image": "mcr.microsoft.com/devcontainers/python:3",
5+
"image": "mcr.microsoft.com/devcontainers/cpp",
66
"features": {
7+
"ghcr.io/devcontainers/features/python:1": {},
78
"ghcr.io/devcontainers/features/node:1": {
89
"nodeGypDependencies": true,
910
"version": "lts"

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ __pycache__/
1515
/fuzz/artifacts
1616
/fuzz/coverage
1717
/fuzz/Cargo.lock
18+
.mypy_cache/
19+
.pytest_cache/

Cargo.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
[workspace]
22
members = [
3+
"allocator",
4+
"allocator/mimalloc-sys",
35
"compiler/qsc",
46
"compiler/qsc_ast",
57
"compiler/qsc_codegen",

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ Code from this repository powers the Q# development experience on <https://quant
2323

2424
## Building
2525

26-
To build this repository there are 4 dependencies that need to be installed. These are:
26+
To build this repository there are dependencies that need to be installed. These are:
2727

2828
- Python (<https://python.org>)
2929
- Rust (<https://www.rust-lang.org/tools/install>)
3030
- Node.js (<https://nodejs.org/>)
3131
- wasm-pack (<https://rustwasm.github.io/wasm-pack/installer/>)
32+
- cmake (<https://cmake.org/>) and a C compiler
3233

3334
The build script will check these dependencies and their versions and fail if not met. (Or run
3435
`python ./prereqs.py` directly to check if the minimum required versions are installed).

allocator/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "allocator"
3+
authors.workspace = true
4+
homepage.workspace = true
5+
repository.workspace = true
6+
edition.workspace = true
7+
license.workspace = true
8+
version.workspace = true
9+
10+
[dependencies]
11+
mimalloc-sys = { path = "./mimalloc-sys" }
12+
13+
[lints]
14+
workspace = true
15+

allocator/mimalloc-sys/CMakeLists.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
cmake_minimum_required(VERSION 3.10.0)
5+
6+
7+
project(allocator_external)
8+
include(ExternalProject)
9+
10+
ExternalProject_Add(mimalloc
11+
GIT_REPOSITORY https://github.com/microsoft/mimalloc.git
12+
GIT_TAG $ENV{ALLOCATOR_MIMALLOC_TAG}
13+
GIT_SHALLOW TRUE
14+
GIT_PROGRESS TRUE
15+
CONFIGURE_COMMAND ""
16+
BUILD_COMMAND ""
17+
INSTALL_COMMAND ""
18+
TEST_COMMAND ""
19+
USES_TERMINAL_DOWNLOAD TRUE
20+
)

allocator/mimalloc-sys/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "mimalloc-sys"
3+
build = "build.rs"
4+
links = "mimalloc"
5+
authors.workspace = true
6+
homepage.workspace = true
7+
repository.workspace = true
8+
edition.workspace = true
9+
license.workspace = true
10+
version.workspace = true
11+
12+
[dependencies]
13+
14+
[lints]
15+
workspace = true
16+
17+
[build-dependencies]
18+
cmake = "0.1"
19+
cc = "1.0"
20+

allocator/mimalloc-sys/build.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use std::boxed::Box;
5+
use std::env;
6+
use std::error::Error;
7+
use std::fs;
8+
use std::path::{Path, PathBuf};
9+
10+
use cmake::Config;
11+
12+
// 1.8.2
13+
//static ALLOCATOR_MIMALLOC_TAG: &str = "b66e3214d8a104669c2ec05ae91ebc26a8f5ab78";
14+
// 2.1.2
15+
static ALLOCATOR_MIMALLOC_TAG: &str = "43ce4bd7fd34bcc730c1c7471c99995597415488";
16+
17+
fn main() -> Result<(), Box<dyn Error>> {
18+
let dst = download_mimalloc()?;
19+
compile_mimalloc(&dst);
20+
println!("cargo:rerun-if-changed=build.rs");
21+
println!("cargo:rerun-if-changed=CMakeLists.txt");
22+
Ok(())
23+
}
24+
25+
// Compile mimalloc source code and link it to the crate.
26+
// The cc crate is used to compile the source code into a static library.
27+
// The cmake crate is used to download the source code and stage it in the build directory.
28+
// We don't use the cmake crate to compile the source code because the mimalloc build system
29+
// loads extra libraries, changes the name and path around, and does other things that are
30+
// difficult to handle. The cc crate is much simpler and more predictable.
31+
fn compile_mimalloc(dst: &Path) {
32+
let src_dir = dst
33+
.join("build")
34+
.join("mimalloc-prefix")
35+
.join("src")
36+
.join("mimalloc");
37+
38+
let mut build = cc::Build::new();
39+
40+
build.include(src_dir.join("include"));
41+
build.include(src_dir.join("src"));
42+
build.file(src_dir.join("src/static.c"));
43+
44+
if build.get_compiler().is_like_msvc() {
45+
build.cpp(true);
46+
build.static_crt(true);
47+
}
48+
// turn off debug mode
49+
build.define("MI_DEBUG", "0");
50+
51+
// turning on optimizations doesn't seem to make a difference
52+
//build.opt_level(3);
53+
54+
build.compile("mimalloc");
55+
56+
println!(
57+
"cargo:rustc-link-search=native={}",
58+
dst.join("lib").display()
59+
);
60+
println!("cargo:rustc-link-lib=static=mimalloc");
61+
}
62+
63+
// Use cmake to download mimalloc source code and stage
64+
// it in the build directory.
65+
fn download_mimalloc() -> Result<PathBuf, Box<dyn Error>> {
66+
let build_dir = get_build_dir()?;
67+
let mut config = Config::new(build_dir);
68+
69+
config
70+
.no_build_target(true)
71+
.env("ALLOCATOR_MIMALLOC_TAG", ALLOCATOR_MIMALLOC_TAG)
72+
.very_verbose(true);
73+
74+
let dst = config.build();
75+
76+
Ok(dst)
77+
}
78+
79+
fn get_build_dir() -> Result<PathBuf, Box<dyn Error>> {
80+
let manifest_dir = env::var("CARGO_MANIFEST_DIR")?;
81+
let build_dir = PathBuf::from(manifest_dir.as_str());
82+
let normalized_build_dir = fs::canonicalize(build_dir)?;
83+
Ok(normalized_build_dir)
84+
}

allocator/mimalloc-sys/src/lib.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use core::ffi::c_void;
5+
pub static MI_ALIGNMENT_MAX: usize = 1024 * 1024; // 1 MiB
6+
7+
extern "C" {
8+
/// Allocate size bytes aligned by alignment.
9+
/// size: the number of bytes to allocate
10+
/// alignment: the minimal alignment of the allocated memory. Must be less than MI_ALIGNMENT_MAX
11+
/// returns: a pointer to the allocated memory, or null if out of memory. The returned pointer is aligned by alignment
12+
pub fn mi_malloc_aligned(size: usize, alignment: usize) -> *mut c_void;
13+
pub fn mi_zalloc_aligned(size: usize, alignment: usize) -> *mut c_void;
14+
15+
/// Free previously allocated memory.
16+
/// The pointer p must have been allocated before (or be nullptr).
17+
/// p: the pointer to the memory to free or nullptr
18+
pub fn mi_free(p: *mut c_void);
19+
pub fn mi_realloc_aligned(p: *mut c_void, newsize: usize, alignment: usize) -> *mut c_void;
20+
}
21+
22+
#[cfg(test)]
23+
mod tests {
24+
use super::*;
25+
26+
#[test]
27+
fn memory_can_be_allocated_and_freed() {
28+
let ptr = unsafe { mi_malloc_aligned(8, 8) }.cast::<u8>();
29+
assert!(!ptr.cast::<c_void>().is_null());
30+
unsafe { mi_free(ptr.cast::<c_void>()) };
31+
}
32+
33+
#[test]
34+
fn memory_can_be_allocated_zeroed_and_freed() {
35+
let ptr = unsafe { mi_zalloc_aligned(8, 8) }.cast::<u8>();
36+
assert!(!ptr.cast::<c_void>().is_null());
37+
unsafe { mi_free(ptr.cast::<c_void>()) };
38+
}
39+
40+
#[test]
41+
fn memory_can_be_reallocated_and_freed() {
42+
let ptr = unsafe { mi_malloc_aligned(8, 8) }.cast::<u8>();
43+
assert!(!ptr.cast::<c_void>().is_null());
44+
let realloc_ptr = unsafe { mi_realloc_aligned(ptr.cast::<c_void>(), 8, 8) }.cast::<u8>();
45+
assert!(!realloc_ptr.cast::<c_void>().is_null());
46+
unsafe { mi_free(ptr.cast::<c_void>()) };
47+
}
48+
}

allocator/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
#[cfg(not(target_family = "wasm"))]
5+
pub mod mimalloc;
6+
7+
/// Declare a global allocator if the platform supports it.
8+
#[macro_export]
9+
macro_rules! assign_global {
10+
() => {
11+
#[cfg(not(target_family = "wasm"))]
12+
#[global_allocator]
13+
static GLOBAL: allocator::mimalloc::Mimalloc = allocator::mimalloc::Mimalloc;
14+
};
15+
}

allocator/src/mimalloc.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use core::alloc::{GlobalAlloc, Layout};
5+
use core::ffi::c_void;
6+
7+
use mimalloc_sys::{mi_free, mi_malloc_aligned, mi_realloc_aligned, mi_zalloc_aligned};
8+
9+
pub struct Mimalloc;
10+
11+
unsafe impl GlobalAlloc for Mimalloc {
12+
#[inline]
13+
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
14+
debug_assert!(layout.align() < mimalloc_sys::MI_ALIGNMENT_MAX);
15+
mi_malloc_aligned(layout.size(), layout.align()).cast::<u8>()
16+
}
17+
18+
#[inline]
19+
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
20+
mi_free(ptr.cast::<c_void>());
21+
}
22+
23+
#[inline]
24+
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
25+
debug_assert!(layout.align() < mimalloc_sys::MI_ALIGNMENT_MAX);
26+
mi_zalloc_aligned(layout.size(), layout.align()).cast::<u8>()
27+
}
28+
29+
#[inline]
30+
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
31+
debug_assert!(layout.align() < mimalloc_sys::MI_ALIGNMENT_MAX);
32+
mi_realloc_aligned(ptr.cast::<c_void>(), new_size, layout.align()).cast::<u8>()
33+
}
34+
}
35+
36+
#[cfg(test)]
37+
mod tests {
38+
use super::*;
39+
use std::error::Error;
40+
41+
#[test]
42+
fn memory_can_be_allocated_and_freed() -> Result<(), Box<dyn Error>> {
43+
let layout = Layout::from_size_align(8, 8)?;
44+
let alloc = Mimalloc;
45+
46+
unsafe {
47+
let ptr = alloc.alloc(layout);
48+
assert!(!ptr.cast::<c_void>().is_null());
49+
alloc.dealloc(ptr, layout);
50+
}
51+
Ok(())
52+
}
53+
54+
#[test]
55+
fn memory_can_be_alloc_zeroed_and_freed() -> Result<(), Box<dyn Error>> {
56+
let layout = Layout::from_size_align(8, 8)?;
57+
let alloc = Mimalloc;
58+
59+
unsafe {
60+
let ptr = alloc.alloc_zeroed(layout);
61+
assert!(!ptr.cast::<c_void>().is_null());
62+
alloc.dealloc(ptr, layout);
63+
}
64+
Ok(())
65+
}
66+
67+
#[test]
68+
fn large_chunks_of_memory_can_be_allocated_and_freed() -> Result<(), Box<dyn Error>> {
69+
let layout = Layout::from_size_align(2 * 1024 * 1024 * 1024, 8)?;
70+
let alloc = Mimalloc;
71+
72+
unsafe {
73+
let ptr = alloc.alloc(layout);
74+
assert!(!ptr.cast::<c_void>().is_null());
75+
alloc.dealloc(ptr, layout);
76+
}
77+
Ok(())
78+
}
79+
}

0 commit comments

Comments
 (0)