How to malloc an array in heap like C?

clang++ 8.0.0-3

The performance compiled by C++ and clang++ is totally same. The performance of Rust and C++ for this demo may depend OS and CPU.

My guess would be that LLVM fails to eliminate bounds checks in this case. To not rely on optimizer for removing bounds checks, consider using iterators:

        for _ in 0..1_000 {
            for (d, (x, y)) in vsum.iter_mut().zip(v1.iter().zip(&v2)) {
                *d = *x * *y;
            }
        }
2 Likes

I tried the same but the performance got even worse :sweat_smile: (also please note += instead of =)

In Rust you malloc an array like this:

let arr = libc::malloc(std::mem::size_of::<i32>() * array_size) as *mut i32;

If you want to use Rust's allocator for this, not C's, then there's allocator API, but it's mostly pointless to try to use it, because Vec is such a thin abstraction, it's basically malloc already.

let vec = Vec::with_capacity(size);
let arr = vec.as_ptr();

is:

struct Vec {data, size, capacity};

inline struct Vec with_capacity(size_t c) {
   return (struct Vec) {
       .data: malloc(c * sizeof(element)),
       .size: 0,
       .capacity: c,
   };
}

and the compiler inlines everything, removes unused code/fields, so in the end you're left with the bare malloc-equivalent.

Note that Box, vec![], Vec::new() and everything else is not equivalent of malloc(), but at best equivalent of calloc() or followed by memcpy.

4 Likes

This benchmark is not testing allocation speed, but bounds checking. Use get_unchecked() to have equivalent of C++'s [], or use .at() in C++ to have equivalent of Rust's [].

13 Likes

I tried your method to alloc memory.

let vec = Vec::with_capacity(size);
let arr = vec.as_ptr();

but when I access the elements in arr using:

*arr.offset(1) as i32

it makes a segment fault.!!!

So does the method with_capacity really malloc memory ?

I'm pretty sure your vec is already dropped at that time. In Rust, borrow checker only checks lifetime of plain references (& and &mut) and pointers are not checked as they are... just pointers. In unsafe context, it's up to you to check lifetime of every pointees, just like we always do in C++.

but how can I keep the pointer to use ?

If I use

let vec = vec![0; size];
let arr = vec.as_ptr();

It works good.

let arr = vec![0; size].as_ptr();

I got a segment fault!!

I don't know why. Any help will be appreciated!

In your second snippet, the vector is dropped at the end of the expression as nobody holds ownership of it. With plain references this kind of mistake leads compile error thanks to our lovely borrow checker, but it's pointer anyway. If you feel familiar with C++, try check the code below.

int* arr = std::vector<int>(size).data();
3 Likes

Please don't try to adapt C++ idioms and habits to Rust.
There's a reason you have to write unsafe to use raw poitners. They are bastards and pure evil. Even if you loose a fraction of a millisecond for every bounds check, you will gain so much, e.g. no (I really mean no) undefined behaviour in safe Rust. This is amazing!

So please, stay on the safe side.

8 Likes

I'm with @hellow on this one. But for the sake of dummy benchmarking comparison, here is @kornel's suggestion of using Vec::with_capacity to allocate memory: you need to mem::forget the obtained Vec or else the heap memory will be freed:

use ::std::mem;

fn malloc<T : Copy> (count: usize) -> *mut T
{
   debug_assert!(mem::size_of::<T>() > 0,
       "manually allocating a buffer of ZST is a very dangerous idea"
   );
   let mut vec = Vec::<T>::with_capacity(count);
   let ret = vec.as_mut_ptr();
   mem::forget(vec); // avoid dropping the memory
   ret
}

EDIT: I'll add a safety disclaimer, given @kornel's comments:

  • Memory leak
    this functions allocates memory and turns it into a raw pointer to prevent Rust from dropping it. It must thus be explicitly freed (see my next post for a way to achieve this).

  • Safety
    Indexing (i.e., dereferencing buf.add(i)) with i: usize is Undefined Behavior if i >= count or if i > ::core::isize::MAX as usize.

  • Safety
    the allocated memory has not been initialized (because that's how C/C++'s malloc work):

    • since this is almost impossible to handle correctly w.r.t to the many places Rust may Drop, a Copy bound is paramount to remain a minimum sane;

    • When T is not an integer type, a value should never be read unless it has been written to before (e.g., using T = bool, reading an uninitialized value may yield a value that is neither 0_u8 nor 1_u8, which is UB; using T = &str, reading an uninitialized value may yield a null value, which is also UB).

2 Likes

Nonononono, please don't use mem::forget() for anything, ever (into_raw, ManuallyDrop, etc. replaced non-hacky uses of it).

Pass around the Vec. Keep it in scope. Don't wrap it in a memory-leaking malloc-like function.

And then, only if absolutely necessary, only in a very limited scope, use vec.as_ptr(). While the vec remains in scope, so it keeps owning the memory.

But I'd like to remind that the whole premise of this post, to use malloc, came out of a mistaken assumption that [] in C++ and Rust are equivalent. The benchmark should have used vec.get_unchecked() or std::vector.at(), and not invent weird ways to allocate the memory (which doesn't make a difference in this case).

15 Likes

Well, if you want to free the allocated memory (but then we are diverging from the dummy example), you need to have:

/// # Safety:
///
///   - The input `buf` must be the returned value of `malloc::<T>::(count)`;
///
///   - `buf` must not have been already freed;
unsafe fn free<T : Copy> (buf: *mut T, count: usize)
{
    let _: Vec<T> = Vec::from_raw_parts(buf, 0, count);
}

Here only ManuallyDrop could have been used to avoid using forget, but it would have been equivalent to doing:

fn look_no_mem_forget (x: T)
{
    let _ = ManuallyDrop::new(x);
}

My example is based on the official documentation's:

Click here to expand the official documentation's example as of May the 3rd, 2019

It depends on the definition of leak: in a way, malloc leaks memory, by design. Calling free() is the way to recover from the leak (a stricter definition of leak would imply that it is not recoverable, in which case using mem::forget on a Vec is not a memory leak, since calling Vec::from_raw_parts does recover from it).


I agree with you that having malloc in Rust defeats the very purpose of the language, and that in this very particular benchmark bound checking is the source of performance (and safety) difference, here. I just think that it would not be honest w.r.t the OP to hide the fact that replicating C/C++'s unsafe functionality is doable (although unadvisable).

I had to tell you my purpose, but I am going to implement a linear algebra library on GPGPU (AMD Vega20). So I must to have know the difference of performance of the vector operation(vector add, vector production) between C++ and Rust. What's more, I must using raw pointer since there is no vector in GPGPU!

I think you are right. The primary purpose is test the performance of vector production which is implemented either in C(int *) or C++(vector) comparing with Rust. I give a wrong demo to make mistake. So you can just forget what I write in C++. I use C(raw pointer) to implement vector production( or array production), and the performance is similar to C++(just a bit better).

My primary question is how can I drop bound check in Rust and I guard that the memory access is ok, tell Rust program to calculate and not to care about anything else.

1 Like

Directly, to guarantee it, is to use .get_unchecked(i) instead of [i].

Indirectly, is to use iterators. Iterators typically avoid unnecessary bounds checks.

for item in vec {}

for (index, item) in vec.iter().enumerate() {}

etc.

There's also a trick that helps prove to LLVM that a slice is large enough, so that it can optimize checks out. You can check whether LLVM can figure it out by testing code on https://rust.godbolt.org

let has_known_length = &vec[0..len];
for i in 0..len {
    has_known_length[i]; // probably not bounds checked anymore, if llvm can see both 0..len
}

but iterators are usually the best solution - safe and fast by default.

6 Likes

thanks a lot.

2 Likes