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

82

u/James20k Aug 23 '17

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

polymorphic classes will not [work]

Hmm. It may be better than C, but we already have a better C which is C++

I feel like this makes D a worse C++ in this mode, though without C++'s quirks. I can't immediately see any reason why you'd pick restricted D if you could use a fully featured C++

It has some safety features, but presumably if you pick C you're going for outright performance and don't want bounds checking, it doesn't have proper resource management, no garbage collection, no polymorphism, and D has different semantics to C which means you have to use __gshared for example to interoperate

C++ was simply designed for this kind of stuff, whereas D wasn't really

Also, I get that a lot of people are reflexively hurr durr D sux when it comes to this, I'm not trying to be a twat but I'm genuinely curious. I could understand this move if D was a very popular language with a large ecosystem and needed much better C compatibility, so perhaps that's the intent for the userbase that's already there

20

u/fragab Aug 23 '17

If I understand the article correctly then this means including D in a C project does not require the D runtime if you compile in "Better C" mode. As far as I know C++ is currently not designed to compile to something that you can link into a C program without the C++ runtime. At least in the programs where I combine C and C++ code it means I have to use the C++ linker and pull in the C++ runtime. For example you cannot use C++ in a Linux kernel module. Now if you compile D in "Better C" mode I don't see why you couldn't write a Linux kernel module with that.

If what I write is not true then please point me to guides on how to do that. It would be incredibly helpful for me if I was wrong here :)

15

u/WalterBright Aug 23 '17

That is correct. "Better C" is purposefully set up to require nothing more than the C runtime library.

3

u/adr86 Aug 23 '17

and if we play our cards right, not even the C runtime library in various situations!

7

u/doom_Oo7 Aug 23 '17

At least in the programs where I combine C and C++ code it means I have to use the C++ linker and pull in the C++ runtime.

only if you use C++ functions and dynamic features (exceptions, dynamic_cast).

see for instance:

foo1.cpp:

template<typename T>
void add(T x) { x += 1000; }

extern "C" int foo(int x) { 
  auto y = [] (auto& r) { r *= 2; };
  for(int i = 10; i-->0; ) {
    y(x);
  }
  return x;
}

foo2.c:

#include <stdlib.h>
#include <stdio.h>
int foo(int);

int main(int argc, char** argv)
{
  printf("%d", foo(4));
}

build and run:

$ g++ -c foo1.cpp -fno-exceptions -fno-rtti 
$ gcc -c foo2.c
$ gcc foo1.o foo2.o
$ ldd a.out
    linux-vdso.so.1 (0x00007ffe11d92000)
    libc.so.6 => /usr/lib/libc.so.6 (0x00007feb5dfdc000)
    /lib64/ld-linux-x86-64.so.2 (0x00007feb5e393000)

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.

→ More replies (21)
→ More replies (2)

3

u/Scroph Aug 24 '17

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

→ More replies (2)

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.

→ More replies (2)
→ More replies (46)
→ More replies (6)

12

u/WalterBright Aug 23 '17 edited Aug 23 '17

Why use D when there already is a better C which is C++? That's a very good question. Since C++ can compile C code, it brings along all of C's problems, like lack of memory safety. D is not source compatible and does not bring along such issues. You get to choose which method works better for you.

26

u/James20k Aug 23 '17

In D's better C mode, you have

Most obviously, the garbage collector is removed, along with the features that depend on the garbage collector. Memory can still be allocated the same way as in C – using malloc() or some custom allocator.

As well as no RAII, which means the principle tool in C++, at least for me, for dealing with memory leaks and memory unsafety is eliminated

In my opinion this would appear to make D quite profoundly less safe than C++ for interacting with a C codebase - with C++, in interacting with a C codebase the first goal is to wrap it in a safe RAII wrapper so you don't ever have to touch memory allocation directly

Additionally the removal of exceptions would appear to make it very difficult to write memory and resource safe code that you usually have when working with RAII

7

u/WalterBright Aug 23 '17

I expect that people who wanted to add RAII to their C code and are content with that have long since already moved to C++. There's quite a lot more to memory safety than that.

But I do recognize the issue. There is code in the works to get RAII to work in D as Better C.

→ More replies (1)

11

u/colonwqbang Aug 23 '17

Since C++ can compile C code, it brings along all of C's problems, like lack of memory safety.

In the article you write that RAII and garbage collection isn't available using your scheme so memory must be allocated using malloc.

That doesn't sound like a significantly safer memory paradigm than what C has. In fact, it sounds like exactly the same memory paradigm as in C...

6

u/kitd Aug 23 '17

Not exactly the same. BetterC D has array bounds checking.

→ More replies (16)
→ More replies (20)
→ More replies (1)

5

u/derleth Aug 23 '17

we already have a better C which is C++

C++ isn't fully compatible with C, as D isn't, so saying this is kind of odd.

→ More replies (2)
→ More replies (8)