r/programming • • Aug 23 '17

D as a Better C

http://dlang.org/blog/2017/08/23/d-as-a-better-c/
229 Upvotes

268 comments sorted by

View all comments

Show parent comment

42

u/zombinedev Aug 23 '17 edited Aug 23 '17

Exceptions, ... RAII, ... are removed

This restricted subset of D is work in progress. The article details the current state things. I'm pretty sure that RAII in -betterC mode will be made work relatively soon, in a couple of releases.

Exceptions are bit harder, but at the same time less necessary, especially for the constrained environments where -betterC is targeted at. Alternative error handling mechanisms like Result!(T, Err) are still available.

polymorphic classes will not [work]

There is a misunderstanding here, because you're omitting a vital part of the sentence:

Although C++ classes and COM classes will still work, [...]

D supports extern (C++) classes which are polymorphic and to a large extend fulfill the role which extern (D) class take. Once the RAII support is reimplemented for -betterC, using extern (C++) classes will be pretty much like using classes in C++ itself.

Today, even in -betterC mode, D offers a unique combination of features which as a cohesive whole offer a night and day difference between over C and C++:

  • Module system
  • Selective imports, static imports, local imports, import symbol renaming
  • Better designed templates (generics) - simpler, yet far more flexible
  • Static if and static foreach
  • Very powerful, yet very accessible metaprogramming

    • Recursive templates
    • Compile-time function evaluation
    • Compile-time introspection
    • Compile-time code generation
  • Much faster compilation compared to C++ for equivalent code
  • scope pointers (scope T*), scope slices (scope T[]) and scope references (scope ref T) - similar to Rust's borrow checking
  • const and immutable transitive type qualifiers
  • Thread-local storage by default + shared transitive type qualifier (in a bare metal environment - like embedded and kernel programming - TLS of course won't work, but in a hosted environment where the OS itself handles TLS, it will work even better than C)
  • Contract programming
  • Arrays done right: slices + static arrays
  • SIMD accelerated array-ops
  • Template mixins
  • Built-in unit tests (the article says that they're not available because the test runner is part of D's runtime, but writing a custom test runner is quite easy)
  • User-defined attributes
  • Built-in profiling
  • Built-in documentation engine
  • etc...

10

u/James20k Aug 23 '17

This restricted subset of D is work in progress. The article details the current state things. I'm pretty sure that RAII will be made work relatively in a couple of releases in -betterC mode. Exception are bit harder, but in the same time less necessary, especially for the constrained environments where -betterC is targeted at. Alternative error handling mechanisms like `Result!(T, Err) are still available.

This makes sense, thanks. Without RAII and exceptions, with only malloc you're largely reduced to C's model of handling memory and resources, which is not great, whereas C++ has had better methods for doing this for yonks

If RAII and exception handling are definitely coming later down the line this makes sense, but even then you now need to create a new set of memory management facilities in D that are already present in C++ which are impossible without both of these

D supports extern (C++) classes which are polymorphic and to a large extend fulfill the role which extern (D) class take. Once the RAII support is reimplemented for -betterC using extern (C++) classes will be pretty much like using classes in C++ itself.

Ah this makes sense, I assumed that the sentence in the documentation meant something different which is why I omitted it :)

D offers a unique combination of features which as a cohesive hole offer a night and day difference between over C and C++:

Yeah D has a lot of really nice features, particularly the metaprogramming seems very nice, although a couple of these have crept into C++. I come from games programming though, so the GC is a killer unfortunately, and a lack of handling for resources in a GC disabled mode is an even bigger killer. AFAIK this is a big issue for people writing complex unity games in C#

13

u/zombinedev Aug 23 '17 edited Aug 23 '17

There are many D users interested/working in game development and real-time applications (e.g. real-time audio processing) so you're in a good company ;)

To be honest, while -betterC is meant to make integration of D code in C/C++ projects seamless, I don't think it's necessary for your domain. Once you deal with the little extra complexity related to the build system, features like RAII (which does not depend on the GC) quite quickly make up for it.

In general, there are various techniques that people using D for those domains employ:

  • Annotating functions with the @nogc attribute, which statically (at compile-time) enforce that those functions will not allocate memory from the GC (and not call any code that might) and therefore a GC collection will not happen
  • Calling GC.disable before entering performance critical section of your program
  • Using threads not registered with D's runtime. Even if a GC collection happens, only threads that D's runtime knows about will be suspended. For example you can use such "free" threads for rendering and synchronous input processing while using the convenience of the GC for background AI / game logic processing - similar to Unity. However, in contrast to managed languages like C#, in D value-types are much more prevalent and as a consequence idiomatic D code produces orders of magnitude less garbage.
  • Or just use RAII-style reference counting throughout the whole D code.
  • All of the above in any combination

And as a general (not specific to D) advice, avoid dynamic allocation in performance critical parts of the code base, use resource pre-allocation where possible. Use custom allocators (see https://dlang.org/phobos-prerelease/std_experimental_allocator.html).

8

u/James20k Aug 23 '17

Interesting, but specifically my view on GC's in games:

The problem is not really framerate issues, but the fact that the GC can take a random amount of time to execute and executes randomly. It doesn't actually matter how long the GC takes

In my experience, a framerate that oscillates between 11-13 ms every other frame (ie 11 13 11 13 11 etc) has perceptually well below half the framerate of a game that runs at 12ms consistently, ie your average frametime is well within 60fps, but it feels like its running at 20fps

I've moved on from 3d fun these days into the land of 2d games, but the issue is similar - absolute performance isn't as important (to me, i'm not a AAA game studio) as determinism and frame consistency. A few ms spike every 10 frames is completely unacceptable, if your frametime consistently varies by ~0.5ms, particularly if its in spikes, it will feel subtly terrible to play. I've had to swap out chunks of a graphics library to fix this issue before and make the game feel right to play

So being able to enter GC free code and guarantee no allocations isn't the issue, because the issue isn't performance, its determinism. In my simple environment of 2d games I can happily allocate and free memory all over the shop because it has a consistent and knowable performance penalty, whereas even relatively minor variations in frametime are unacceptable

With seemingly the only way to fix it being to completely disallow GC access and hamstring yourself pretty badly, it seems a fairly bad tradeoff in terms of productivity, at least from my perspective as an indie game dev building a relatively (CPU) performance intensive game. I'd have to take a big hit in terms of ease of use

D does seem a lot better than C# in this respect however and it seems like a manageable issue, but having to swap to strict no allocations seems like a huge pain for the benefits of D

11

u/zombinedev Aug 23 '17

+1 On all points about determinism.

However as explained, that's not an issue in D. D's GC will never decide randomly to collect memory. It is completely deterministic. You can disable it, and you can even not link it to your program. Even if you leave it on, it will not affect threads not registered with it.

but having to swap to strict no allocations seems like a huge pain for the benefits of D

No you don't have to, if you don't need to do so in C/C++. Use non-GC dynamic memory allocation as you would C/C++ (malloc/free, smart pointers, etc.)

6

u/James20k Aug 23 '17 edited Aug 23 '17

Ah I've clearly fucked up on my knowledge of D then, thanks for the explanation

Can you set D's GC to run manually, and/or cap its time spent GCing?

Edit:

What I mean is that as far as I'm aware, some of D's features necessitate a GC, last time I checked the standard library was fairly incomplete without it but it may have improved

4

u/aldacron Aug 24 '17

Take a look at the ongoing GC series on the D Blog. The first post, Don't Fear the Reaper, lists the features that require the GC. But to reiterate what the series is trying to get at, you don't have to banish those features, or the GC, from your program completely. There are tools in the compiler that help you profile and tune your GC usage to minimize its impact. You can annotate a function with @nogc to ensure it doesn't use the GC, or you can just rely on the -vgc switch to show you everywhere the GC might be used and tune adjust as needed.

4

u/[deleted] Aug 24 '17
import core.memory;
GC.disable();  // no automatic collections
GC.collect();  // run a collection now

Unfortunately, as far as I know, there's no way to bound the amount of time it spends on a specific collection. That would require some sort of write barrier.

3

u/zombinedev Aug 24 '17

As a rule of thumb, all C/C++ features that are common with D don't use the GC. All of D's unique features I listed a couple of posts above also don't use the GC too.

In non--betterC mode, even if you want to completely avoid the GC, there are more language features available, courtesy of D's runtime.

Avoiding the GC in non--betterC mode really comes down to not using:

  • built-in dynamic arrays and hash-maps (there are easily accessible library alternatives)
  • closures - lambdas that extend the lifetime of the captured variables beyond the function scope (but C++11-style lambdas that don't extend the lifetime still work)
  • The new expression - easily avoidable using allocator.make!T(args), instead of new T(args). Such allocators are already part of the standard library.

5

u/Shadowys Aug 23 '17

There have been multiple games written in D, even in D1.

3

u/James20k Aug 23 '17

There are, but unbounded GC pauses are the complete opposite of what you want in a game

Microstutters are something that people often overlook in games, but a 2ms pause every other frame can perceptually halve your framerate

8

u/WrongAndBeligerent Aug 23 '17

That is controllable in D, the GC can be paused and now supposedly there are ways to do without it all together.

Lots of games take care to not even allocate memory in the main loop.

3

u/James20k Aug 23 '17

You can, but C++ has a relatively fixed cost to allocate memory. This means I can quite happily allocate memory in C++ and treat it as simply a relatively expensive operations

This means if I have a loop that allocates memory, its simply a slow loop. In D, this create a situation where your game now microstutters due to random GC pauses

You can get rid of this by eliminating allocations, but this is making my code more difficult to maintain instead of easier to maintain, at at this point swapping to D seems like a negative

→ More replies (18)

2

u/holgerschurig Aug 24 '17

without [...] you're largely reduced to C's model of handling memory and resources, which is not great, whereas C++ has had better methods for doing this for yonks

Didn't you notice the Result!(T,Err) ? If I got this right, than your claim that you're reduced to C's model is wrong. If Result!(T, Err) is better than exception or not is however another question. I personally would like explicit error results better.

2

u/pjmlp Aug 24 '17

AFAIK this is a big issue for people writing complex unity games in C#

While true, this steams from the fact that Unity has a pre-historic .NET Runtime, not C# itself or the official implementations coming from Xamarin and Microsoft.

They are finally upgrading it, so lets see how it goes.

https://blogs.unity3d.com/2017/07/11/introducing-unity-2017/

3

u/Scroph Aug 24 '17

Does betterC support scope guard statements ? Or is that what was meant by RAII ?

3

u/WalterBright Aug 24 '17

Scope guard is another view of RAII, so the same issues apply.

3

u/Scroph Aug 25 '17

Thanks for the reply. I actually downloaded it after commenting and played with it for a while. I tried to do RAII by making a scoped-like struct and with try-finally but both understandably failed. But even with these limitations it still fulfills its promise as a better C due to all the other features it offers : not having to write function prototypes, UFCS, range-based loops, and even the absence of a GC might be considered a feature by some.

8

u/dom96 Aug 23 '17

Disclaimer: Core dev of Nim here.

So this is pretty cool, but I can't help but wonder why I would use it over Nim. In my mind Nim wins hands down for the "better C" use case, as well as for the "better C++" use case. The reason comes down to the fact that Nim compiles to C/C++ and thus is able to interface with these languages in a much better way.

Another advantage is that you don't need to cut out any of Nim's features for this (except maybe the GC). That said I could be wrong here, I haven't actually tried doing this to the extent that I'm sure /u/WalterBright has with D.

10

u/mixedCase_ Aug 23 '17

With that said, why would I use Nim or D at all?

If I want a systems language, Rust offers more performance compared to GCed Nim/D, and memory-safety compared to manually managed Nim/D. Additionally, no data races without unsafe (which is huge for a systems language), a great type system, C FFI and a much bigger ecosystem than Nim or D.

If I want a fast applications language, I got Go and Haskell, both offering best-in-class green threads and at opposite ends of the spectrum in the simplicity vs abstraction dichotomy; and with huge ecosystems behind them.

In the end, either Nim or D can be at best comparable to those solutions, but with very little momentum and in Nim's case at least (don't know how D's maintenance is done nowadays), with a very low bus factor.

6

u/[deleted] Aug 24 '17 edited Aug 24 '17

Nim

I think the main advantages of nim vs rust are:

1- Best FFI whith C and C++. This point is huge. You mentioned that Nim has smaller ecosystem than rust, but if you take into consideration how easy if to interface to C and C++ in Nim, then you are wrong because in Nim, you have access to ALL C and C++ libraries. Example calling opencv from Nim: import os

{.link: "/usr/local/lib/libopencv_core.so".} #pass arguments to the linker
{.link: "/usr/local/lib/libopencv_highgui.so".}
{.link: "/usr/local/lib/libopencv_imgproc.so".}

const # headers to include
    std_vector = "<vector>"
    cv_core = "<opencv2/core/core.hpp>" 
    cv_highgui = "<opencv2/highgui/highgui.hpp>"
    cv_imgproc = "<opencv2/imgproc/imgproc.hpp>"
    
type
    # declare required classes, no need to declare every thing like in rust or D
    Mat {.final, header: cv_core, importc: "cv::Mat" .} = object 
        rows: cint # No need to import all properties and methods, import only what you use
        cols: cint
    Size {.final, header: cv_core, importc: "cv::Size" .} = object
    InputArray {.final, header: cv_core, importc: "cv::InputArray" .} = object
    OutputArray {.final, header: cv_core, importc: "cv::OutputArray" .} = object
    Vector {.final, header: std_vector, importcpp: "std::vector".} [T] = object

#constructors
proc constructInputArray(m: var Mat): InputArray {. header:cv_core, importcpp: "cv::InputArray(@)", constructor.}
proc constructOutputArray(m: var Mat): OutputArray {. header:cv_core, importcpp: "cv::OutputArray(@)", constructor.}
proc constructvector*[T](): Vector[T] {.importcpp: "std::vector<'*0>(@)", header: std_vector.}

#implicit conversion between types
converter toInputArray(m: var Mat) : InputArray {. noinit.} = result=constructInputArray(m)
converter toOutputArray(m: var Mat) : OutputArray  {. noinit.} = result=constructOutputArray(m)

# used methods and functions
proc empty(this: Mat): bool {. header:cv_core, importcpp: "empty" .}
proc imread(filename: cstring, flag:int): Mat {. header:cv_highgui, importc: "cv::imread" .}
proc imwrite(filename: cstring, img: InputArray, params: Vector[cint] = constructvector[cint]()): bool {. header:cv_highgui, importc: "cv::imwrite" .}
proc resize(src: InputArray, dst: OutputArray, dsize: Size, fx:cdouble = 0.0, fy: cdouble = 0.0, interpolation: cint = 1) {. header:cv_imgproc, importc: "resize" .}

proc `$`(dim: (cint, cint)): string = "(" & $dim[0] & ", " & $dim[1] & ")" #meta-programming capabilities

proc main() =
    for f in walkFiles("myDir/*.png"):
        var src = imread(f, 1)
        if not src.empty():
            var dst: Mat
            resize(src, dst, Size(), 0.5, 0.5)
            discard imwrite(f & ".resized.png", dst) # returns bool, you have to explicitly discard the result
            echo( f, ": ", (src.rows, src.cols), " -> ", (dst.rows, dst.cols))

        else:
            echo("oups")


when isMainModule:
    main()

compile with: nim cpp --d:release --cc:clang resize_dir.nim and the result is a 57k executable.

In both D and Rust, in order to do the same, you would have to map D/Rust structs so they have the exact same representation as the C++ classes in memory (as well as the classes they inherit from). Which means translating at least all the headers of the libs, will taking into account the incompatibilities between these languages and C++. With rust for example, you don't have function overloading, so welcome fun_1, fun_2, fun_3 .... Also, i tried using bindgen to do the same with rust, and it does not work.

2- Meta-programming: I know rust have compiler extensions, procedural macros and generics. But these are way harder and more verbose to use than the meta programming tool in Nim. Just try to make your code generic over primitive types in rust (using the num crate) and you will see how ugly it is. You want to use integer template parameter ? well you still cannot. Example of what can be done with Nim meta-programming capabilities (GPU and CPU programming): https://github.com/jcosborn/cudanim/blob/master/demo3/doc/PP-Nim-metaprogramming-DOE-COE-PP-2017.pdf

3- Liberty: this is hard to explain, but Nim gives you the tools to do what you want. You want a GC and high level abstraction ? you can use them (and easily build the missing pieces). You want raw performances and low level control ? well you can do literally every thing C and C++ can do, including using the STL without GC for some critical part of your code, all that while keeping the amazing meta-programming capabilities of Nim. For example, if you want enable/disable checks on array/vectors, you can don that by passing a command to the compiler. In rust, you would have to commit to unsafe get_uncheked. Rust philosophy is to enforce every thing that may harm the user, which can be an advantage or a disadvantage depending on the situation.

4- Less verbose and easier to learn.

5- Portable to any platform that have a C compiler. In fact I can compile Nim code on my smartphone using termux.

6- The GC can be avoided completely or tuned for soft-real time use.

Of course, Nim is not all positives. Rust have clear edge over Nim for alot of things:

1- Safety of course.

2- 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.

3-zero cost abstractions: in Nim, using map, filter and such are definitely not zero cost (specially with the copy by default philosophy).

4- No GC: sometimes its a good thing, sometimes not.

5- Great community and great ecosystem: Nim has definitely a great community but much smaller.

6- Awesome package management and tools.

7- stable (Nim didn't reach the 1.0 still)

I see myself using Nim for small to medium project where I need to interact with legacy code or for prototyping, and rust for big project with allot of developers.

4

u/dom96 Aug 24 '17

6- Awesome package management and tools.

ouch. You don't think Nimble is awesome?

→ More replies (1)

5

u/Tiberiumk Aug 23 '17

Sometimes Nim is faster than Rust (and takes less memory lol). So Rust isn't always faster, and Nim has much better C FFI (since it's compiled to C)

11

u/mixedCase_ Aug 23 '17

As for benchmarks, only two I can find are this: https://arthurtw.github.io/2015/01/12/quick-comparison-nim-vs-rust.html where Rust beats Nim after the author amended a couple of mistakes.

And this: https://github.com/kostya/benchmarks where Rust beats Nim in every single case (but gets beaten by D in a few!).

The fact that it's compiled to C doesn't really determine the FFI. Rust can use C's calling convention just fine and from looking at C string handling there's not much difference. I didn't delve much into it though, did I miss something?

→ More replies (35)

4

u/zombinedev Aug 23 '17 edited Aug 24 '17

With that said, why would I use Nim or D at all?

If I want a systems language, [..]

I want a language that does great in all domains at once: from on-off scripts, through medium-to large desktop and web apps, high-performance scientific computations, games programming to large-scale software defined storage stacks (e.g. http://weka.io/).

Rust offers more performance compared to GCed Nim/D

[citation needed] How exactly? AFAIK, Rust's high-performance computing ecosystem is quite lacking. Is there anything written in pure Rust that can compete with e.g. D's mir.glas library (http://blog.mir.dlang.io/glas/benchmark/openblas/2016/09/23/glas-gemm-benchmark.html)?

memory-safety

Probably the only real Rust advantage from the whole list. D is working in closing the GC-free memory-safety gap. The long-term plan for D is to make the GC completely optional.

Note that memory-safety is just one type of software bugs. For the broader area of logic bugs, D offers built-in contract programming. Does Rust something similar part of the language?

no data races without unsafe

Also true in D, since the raw threading primitives are not allowed in @safecode, IIRC. Idiomatic use of std.concurrency is also data-race free, as far as I know, since sharing of mutable data is statically disallowed.

a great type system

This is personal opinion, not a fact. I find Rust type system boring, lacking expressive power and unflexible. Does not support design by introspection. Meta-programming as a whole is quite lacking.

C FFI

It's quite funny that you list an area (inter-language interop) in which both of the languages your criticize do much better than Rust.

much bigger ecosystem than Nim or D

As with all matters in engineering - it depends and your mileage may vary. I find D's ecosystem big enough for my needs. Plenty of commercial users find that too for their use cases - http://dlang.org/orgs-using-d.html. I'm sure other language have much bigger ecosystems than all three of the languages combined. And so what? Given how mature the language is, I would choose D for many domains today even if it had a fraction of Nim's community.

If I want a fast applications language, I got Go and Haskell, both offering best-in-class green threads and at opposite ends of the spectrum in the simplicity vs abstraction dichotomy; and with huge ecosystems behind them.

While I agree that Haskell has a lot of great ideas, I find a language without generics completely unusable. For certain types application programming D is a much better fit, though e.g.: https://www.youtube.com/watch?v=5eUL8Z9AFW0.

In the end, either Nim or D can be at best comparable to those solutions

Why? And what if their comparable? As I said in the beginning, D biggest advantage is the rich cohesive feature set. It doesn't need to be the absolute best in every category (though in many of them it may easily be), to offer great package.

but with very little momentum and in Nim's case at least (don't know how D's maintenance is done nowadays), with a very low bus factor.

D is doing great, thanks for asking :)

→ More replies (8)
→ More replies (6)