Thanks all for your help!

I wrote an post on reddit to explain what may be the advantages of using Nim (from my point of view) over rust and showing how to use opencv from Nim.

2017-08-24 09:39:44

There is one thing where I would like to add my two cents:

Copy and move semantic which are a delight to use when writing performance critical code specially. I think Nim is poor in that regards as it deeps copies seq (vectors) and string by default

It is true that Nim does not have move semantics like Rust, but I see a clear advantage that arguments to functions are passed probably by const reference.

++

struct MyStruct {
  int64_t a,b,c,d;
};

void foo(const MyStruct& arg) {
  // ...
}

Is in Nim:

type
  MyObject = object
    a,b,c,d: int64

proc foo(arg: MyObject): void =
  #...

Therefore a lot of copying is avoided by default. Instances wher you actually need to copy a value are rare. And there is less visual noise compared to the Rust and C++ version.

2017-08-24 12:49:25

Yes allot of copying is avoided by default when passing arguments to a function and with less noise.

However, as my original question in this thread indicates, it is in the body of the function that things my get complicated (ex: when using standard containers like dequeue of seqs).

I find the rust solution to be quite beautiful:

Argument can be passed by reference (&), by mutable reference (&mut) or by value. In the first two cases, no copy is done and in the case of &mut, the object passed can be modified (with all the borrowing rules about &mut). But when the object is passed by value, there is two options:

1- If the "object" implements the Copy trait, it is copied (like all primitive types).

2- If it does not implement the Copy trait, the object is moved, and the compiler will forbid using it any further. ex:

let v = vec![1, 2, 3];
let mut vv : Vec<Vec<i32>> = Vec::new();
vv.push(v);
println!("{:?}", v);

# generates the following error
# error[E0382]: use of moved value: `v`
# --> src/main.rs:6:22
#  |
#5 |     vv.push(v);
#  |             - value moved here
#6 |     println!("{:?}", v);
#  |                      ^ value used here after move
#  |
#  = note: move occurs because `v` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait

Which means that for "big objects", there is NEVER any copy if not explicitly stated with a "v.clone()". This enables the compiler to do some nice optimizations and make it easier for the developer to think about performances.

In C++, it is less elegant (you have to call std::move explicitly) and moved objects can still be accessed. But it still an important tool for optimization.

In Nim, i don't know if there is a way to tell the compiler "hey I don't need this values here anymore, take it instead of copying it"

2017-08-24 14:11:09

In Nim, i don't know if there is a way to tell the compiler "hey I don't need this values here anymore, take it instead of copying it"

IMHO, that's all down to memory reference. I think the object which resides in memory doesn't directly pointed instead it's pointed by a pointer. Let's think it a box which has the "value" that points where the actual object resides.

So for example var a has the memory address at 0xff00 which when dereference it would return another address which the actual object resides, let's say it's in 0x1000. So if we want to move the memory to var b, we would give "value" of b saves 0x1000, while the memory of b is 0xfff0, so if we want to discard the a, we would give a = nil so if any further usage of a would yield an error. While the usage of b simply unpack the data which resides in 0x1000.

CMIIW, with template and/or macro, you should be able to replicate those. Primitive value said easily copied because it's usually simple literal value or the type which already available for cpu targets.

Also, for copying and/or modifying, simply use ptr would solve your initial problem although it would spawn another problem.

NB: please correct above example if you find the example is wrong, I just posted in in the spot without thinking it deeply

2017-08-24 14:51:52

thanks for your response. For my problem I ended up putting my seq in an object with the {.shallow.} attribute as suggested by Jehan.

My nim implementation still slower (by not so much) than my rust version. After seeing the generated C code, it my be due to allot of memset(0) (some times two consecutive memset on the exact same memory region, or memset followed directly by a memcpy).

It is not a big problem, it just means the code C/C++ code generation backend can benefit from some improvement.

I tend to avoid using ptr as it is not managed by the GC by my understanding.

2017-08-24 15:03:50
BigEpsilon, would the {.noinit.} pragma help for the memset(0)? 2017-08-24 15:25:29

Compile with "--opt:speed" or "--opt:size" and take a look at the binary. I think there's a good chance you won't find your successive memset() calls.

In my experience, GCC and Clang optimize sequences of calls to stdlib functions, in which some would affect a no-op, well.

I could be wrong, but I'd take a look before trying to optimize the generated C code.

2017-08-24 15:59:56

Rust and Nim actually have the same goal here, enforcing a "mutability xor sharing" invariant; i.e. a value can either be shared or mutable, but not both. They just do it somewhat differently.

Rust enforces it via the borrow checker, Nim by turning assignments to mutable locations into copies. To either work around the constraint or to violate it generally involves jumping through extra hoops.

First, note that Nim does an automatic copy only when putting a shared value in a mutable location, which is normally not possible in Rust, outside a move assignment.

But you can do move assignments in Nim, too, same as it's done in C++:

template move*[T](x, y: var T) =
  swap x, y
  reset y

Now, nobody does it this way in Nim, because it's unnecessarily expensive and because shallowCopy or a swap without the reset is usually more than adequate and cheaper, even though it (temporarily) violates the invariant.

This is because the most common use case for move assignments is to store local values in the heap, and the invariant violation ends once the local value goes out of scope.

In order to do the same in Rust, you have to use Cell (which does a copy, same as Nim`s mechanism) or RefCell (which adds an extra flag to the value in order to check whether it has been borrowed or not, replacing static with dynamic borrow checks).

Conversely, Nim can also avoid copies. Passing arguments and returning results already avoids copying. When reading values from the heap, using let in lieu of var also avoids copying; same goes for using let instead of var locally if you don't mean to mutate the result.

Note that you can also avoid copying on strings and seqs in Nim if you mark them as sharable by using the shallow primitive. Example:

proc main =
  var a = "foo"
  shallow a
  var b = a # Look, ma, no copy!

You do this when you want to say that it's always safe to share this value, e.g. because it's immutable.

2017-08-24 20:35:03

In my experience, GCC and Clang optimize sequences of calls to stdlib functions, in which some would affect a no-op, well.

Correct. Both gcc and clang turn memset() calls into __builtin_memset() intrinsics, which the compilers know to treat as assignments.

2017-08-24 20:36:48

Thanks all for your replies.

@Jehan: your responses are always very informative, thank you ! it is now clear for me that the {.shallow.} pragma, shallow and shallowCopy calls can do the job of move (in most cases), and that move is more related to how c++ works.

Just as a side note, I didn't succeed at making my nim code as fast as my rust version, so I just replaced seqs by warped c++ vectors (with some moves in that case ) in the hot spots and now the nim version is slightly faster than the rust version.

2017-08-27 13:33:48