HNHacker News
TopNewBestAskShowJobs

arc776

256 karma · joined January 28, 2016

submissionscomments
arc776··on Nim Community Survey 2021 Results
What things do you wish for in Nim from Rust?
arc776··on Nim 1.6
The full changelog is here: https://github.com/nim-lang/Nim/blob/version-1-6/changelogs/...

For those who wonder what the 15 extra modules are, here's a quick summary:

Algorithms:

  - enumutils: generate case statements from enums, and utils for holey enums.
  - setutils: adds functionality for the built in `set` type.
  - packedsets: replacement for `intsets` that allows distinct types.
Operating system:

  - tasks: basic primitives for creating parallel programs.
  - sysrand: create cryptographically secure random numbers.
  - tempfiles: create temporary files and directories.
  - vmutils: enable/disable VM tracing in user code.
Javascript:

  - jsbigints: arbitrary precision integers.
  - jsfetch: wrapper for the JS Fetch API.
  - jsformdata: FormData wrapper for the JS target.
  - jsheaders: Headers wrapper for the JS target.
Metaprogramming:

  - genasts: alternative to `quote` for metaprogramming with explicit binding.
Misc:

  - socketstreams: wraps sockets as streams.
  - importutils: allows access to private fields from another module.
  - strbasics: high performance string operations.
You can check out the modules themselves at the stdlib page: https://nim-lang.org/docs/lib.html

There's also loads of quality of life improvements across the board to existing standard library modules and general backend, e.g.:

  - float to string is 10x faster,
  - JSON serialisation is 20x faster,
  - better compiler messages,
  - memory management performance (e.g., ORC is 10% faster)
  - `static[T]` improvements,
  - improve `enum` type conversions and allow `enum` overloading,
  - new design `concepts` that compile faster,
  - build system and cross compilation improvements,
  - unicode operator symbols,
  - VM improvements such as working with `addr` at compile time.
arc776··on Nim 1.6
> it is a garbage collected language

GC has to be explicitly attached to types. By default everything is a value type allocated on the stack, and managed by scope. Nim is also clever enough to optimise away copies for value types (such as passing immutable parameters). GC is only really used for reference semantics.

> garbage collection is not really optional

Sure it is. Some of the stdlib uses GC for dynamic lists, but if you're after ultimate control you can easily make your own dynamic lists using manual memory management thanks to the type system and move semantics.

> ARC will still leak memory when there are cycles, so it can work if you are very careful about how you manage data.

If you have cycles and want GC, as you mention, you'd use ORC. As a point of comparison, Rust references also leak with cycles https://doc.rust-lang.org/book/ch15-06-reference-cycles.html

> But it's trickier than what you get out of modern C++, and more resource-hungry than full manual memory management, so I can't really see this option being ideal for gamedev or embedded.

I'm curious how ARC/ORC are tricky to use? Currently it's just a compile switch (soon to become default). There's not really any 'usage' at all, it just switches assignment to move semantics where possible.

> I can't really see this option being ideal for gamedev or embedded.

My personal experience is that ARC/ORC are extremely performant. They don't "stop the world" like Java/Python and are designed to be suitable for embedded work.

In particular ARC offers "deterministic performance for hard realtime systems". For ORC and other GCs you can manually step collection and define soft-realtime collection pause limits. You can even plug in other GC implementations, or create your own if you have specific requirements.

The memory model https://nim-lang.org/docs/gc.html states:

    Nim provides multiple paradigms for needs ranging from large multi-threaded applications, to games, hard realtime systems and small microcontrollers.
As an example of a large and complex project running on embedded and using GC, see the Nimbus Ethereum client. Embedded is actually a big use case for Nim specifically because of how memory and CPU efficient the language can be, and how tunable everything is.

Besides, generating a lot of garbage each frame is a design issue in gamedev. Normally you'd preallocate or at least chunk allocate.

> ORC is basically just Python's garbage collection algorithm

ARC is more similar to Rust's move semantics or C++'s smart pointers, and ORC just adds a cycle collector on top. You can also mark types as `acyclic` to remove cycle collection by type.

What makes it ideal for gamedev is high productivity, run time execution speed, interfacing with C/C++ natively, and tools like AST macros.

> For shell scripting, at the end of the day, it is still a statically typed, compiled language. This just doesn't hit the sweet spot for a scripting language ... the entire operating context is irredeemably weakly typed, and nothing you do will ever grow large enough for static types to help much with maintainability.

I think scripting being "irredeemably weakly typed" is a matter of opinion. With good type inference, you get almost all of the advantages of dynamic types, such as fast edit-test cycles, without the pain of not knowing what anything is. Weak typing is ultimately just how you define converters, and I prefer making that explicit rather than lenient (libraries can make typing effectively 'weaker', e.g., https://nim-lang.org/docs/lenientops.html ).

There's nothing you can't do in statically typed languages that you can in dynamically typed languages. The only disadvantage Nim has over, say, Python, is it's weaker REPL support for now (hot code reloading is WIP: https://nim-lang.org/docs/hcr.html ).

The nimscript subset of the language available at compile time is actually really good for scripting on its own, and is used for scripting builds without needing a separate language: https://nim-lang.org/docs/nims.html

arc776··on Nim 1.6
Nim has a popular ML library "inspired by Numpy and PyTorch": https://github.com/mratsim/Arraymancer

One of the advantages of the language for DS/ML is the native "C like" performance with very low developer friction.

Another advantage from a library perspective is being able to automate fast, low level boiler plate code from easy to use DSLs using AST macros. For example a DSL could generate bespoke code for different data layouts and pipelines etc., giving you the best possible performance without the user worrying about it.

arc776··on Nim 1.6
Being able to compile to C, C++, ObjC, and JavaScript natively (and LLVM using https://github.com/arnetheduck/nlvm ), along with an excellent FFI (including to and from Python) means you don't need to rewrite dependencies as you can use them directly. Nim is great at glue code - arguably better/easier than Python.

Along with general language characteristics such as being high level and productive like Python, but with intricate "bare metal" control when you want it, really does make it suitable for writing almost everything.

arc776··on Am I stuck in a local maximum?
> shoving all your data into and out of boxes that don't fit

Ironically this is what dynamic languages do. Everything is in boxes that could be anything.

It's worth bearing in mind that dynamic languages can be (and often are) built using statically typed languages. You can easily build dynamic collections using static typing.

arc776··on Am I stuck in a local maximum?
Why do you want to have a static type for each combination of fields in a query? There's no need to do this.

> I WISH there was a dynamic language that would statically type your attributes, that would be the perfect hybrid for me.

In statically typed database frameworks it's often the other way round: you have pseudo-dynamic typing for fields but they're static under the hood, sub-typed for the specific database type and/or stored as raw bytes.

This is basically "boxing" fields - exactly the same principle as dynamically typed languages do behind the scenes, except the library will have customised "boxing" designed for the subset of types the database supports, e.g., typed per field column rather than for every individual field in the result (like dynamic languages). This hugely reduces the overhead of actual boxing as you don't need to store the type of the field or indirect to its data.

This is sort of like dynamic typing just for the database, except the possible types are focused on a specific subset the database supports and therefore can be made efficient for the task.

Dynamically typed languages must cater for reassigning properties, fields, and/or types ad hoc, and must be made much more general - AKA slow.

For example you could (in pseudocode) do:

    procedure showTitles(queryResults: QueryResults, titleName: string):
      for title in queryResults.fieldData(titleName):
        let str = title.getString # Returns a string type.
        display str

    let queryResults = db.query("SELECT * FROM MYTABLE")
    showTitles(queryResults, "title")
You get the benefits of static typing and dynamic typing; type errors are caught at compile time, and you explicitly or implicitly convert the "dynamic" box for the field. For instance, trying to pass an object that isn't a `QueryResult` to the `showTitles` procedure will throw a compile time error. In dynamic languages, you could pass anything to that procedure and have to hope it doesn't blow up at run time. Testing all the possible paths and dynamic types that could be given to this procedure could be a combinational nightmare, so "to be safe" you'll have to check the type is the equivilent to a `QueryResult` anyway at run time...

The `getString` function can do whatever it needs to do to convert the data if it's not stored as a string (or throw an error if this isn't possible/appropriate), whilst maintaining type relationships once it's out of the pseudo-boxed type and ensuring memory used is appropriate to the "real" type.

Source: I've written database query frameworks for statically typed languages.

arc776··on Prologue: A web framework written in Nim
Fair enough, I feel like some antialiasing on the fonts would make it look a lot nicer.
arc776··on Attempts to make Python fast
Of course bigints require more than one instruction to add them, but even then you can reduce the work at compile time down to a series of integer operations, whereas the above code requires interpretting the program before it even gets to the add.

In your example text processing in `unicode_concatenate` is going to be very, very much slower than a bulk load of the native numerical data directly from memory and processing it. For each character, Python needs to check a number is still a number at run time then convert the result to a native numeric. I can only assume this string processing is at worst performed once and cached(?), because otherwise it doesn't seem like it would run well at all and surely Python's bigint performance is pretty important.

> Python code can be made more high performance if there's some way to tell the implementation the types, either explicitly or by inference or tracing.

At that stage, I would just use Nim and get better performance and a decent static type system included and either call it from Python, or call Python from Nim.

arc776··on Attempts to make Python fast
Ah, that's a good point. It would be interesting to see fairer benchmarks for TruffleRuby after reading about some of the optimisations they're able to make.
arc776··on Attempts to make Python fast
Thanks for the links. Very interesting to read how you get around these tricky dynamic operations!

> get threads that want to monkey patch to stop other threads and tell them to start checking for redefined methods

As an aside, this sort of reminds me of branch prediction at a higher level. A very neat way to speed up for the general case of no patching.

> This is a great example because people said 'surely... surely... there will always be some overhead to check for monkey patching - no possible way to solve this can't be done' until people found the result already in the literature that solves it.

There is still overhead when patching is used though. If you don't use the feature, you don't pay the cost, however when monkey patching is used there is a very definite cost to rewriting the JIT code and thread synchronisation that compiled languages would simply not have.

I can see where you're coming from here. If we can reduce all dynamic costs that aren't used to nothing then we will have the same performance as, say, C. At least, in theory.

It would be certainly be interesting to see a dynamic language that can deoptimise all its functionality to a static version to match a statically compiled language. Still, any dynamic features would nevertheless incur an increased cost over static languages.

It's the dynamism itself of Python that incurs the performance ceiling over static compilation, plus the issues I mentioned in my previous reply about boxing and cache pressures. However you've definitely given me some food for thought over how close the gap could potentially be.

arc776··on Attempts to make Python fast
> Can you give specific examples and prove that they cannot be overcome?

It's hard to prove a theoretical negative, but perhaps by comparison with the run time performance of static AOT (SAOT) compiled languages I can show what I mean.

Dynamic typing:

- Python requires dynamic type checking before any program work is done. SAOT doesn't need to do any run time work.

- Adding two numbers in Python requires handling run time overloads and a host of other complexities. SAOT is a single machine code instruction that requires no extra work.

Boxing values:

- Python value boxing requires jumping about in heap memory. SAOT language can not only remove this cost but reduce it to raw registers loaded from the stack and prefetched in chunks. This massively improves cache performance by orders of magnitude.

Determinism:

- In Python program operation can only be determined by running the code. In SAOT, since all the information is known at compile time programs can be further folded down, loops unrolled, and/or SIMD applied.

Run time overheads

- Python requires an interpretive run time. SAOT does not.

In summary: Python necessarily requires extra work at run time due to dynamic behaviour. SAOT languages can eliminate this extra work.

I do understand though that with JIT a lot of these costs can be reduced massively if not eliminated once the JIT has run through the code once. For example here they go through the painful process of optimising Python code to find what is actually slowing things down, to the point of rewriting in C: http://blog.kevmod.com/2020/05/python-performance-its-not-ju...

At the end they point out that PyPy gives a very impressive result that is actually faster than their C code. Of course, this benchmark is largely testing unicode string libraries rather than the language itself and I'd argue this is an outlier.

> How much of the literature have you read?

Literature on speeding up Python or high performance computing? The former, very little, the latter, quite a lot. My background is in performance computing and embedded software.

I'm definitely interested in the subject though if you've got some good reading material?

> people said monkey-patching in Python and Ruby was a hard overhead to peak temporal performance and fundamentally added a cost that could not be removed... turns out no that cost can be completely eliminated.

This really surprised me. Completely eliminated? I'm really curious how this is possible. Do you have any links explaining this?

arc776··on Attempts to make Python fast
> I'd challenge the idea that there really are known 'limits'.

There are good reasons why these limits cannot be overcome in that the complexity and dynamism of the language precludes it.

Being interpreted is one cost that sets a significant barrier to performance, and the dynamic complexity further compounds it. For example whereas JS is basically only functions, in Python you have a huge range of ways you can do incredibly complex things with slot wrappers, descriptors, and metaprogramming.

Ultimately, Python will get faster, but diminishing returns are inevitable. Python can never be as fast as the equivalent code in a compiled language. It simply has too much extra work to do.

arc776··on Attempts to make Python fast
I'm not familiar with Ruby, but it doesn't seem like TruffleRuby is really competitive with languages known for performance.

These are only simple benchmarks, but do indicate a rough ballpark for TruffleRuby: https://github.com/kostya/benchmarks

As I understand it, Crystal would be a good Ruby alternative if you want performance. This is of course a whole new language designed with performance in mind from the beginning and here is a repeating theme: you need to consider performance at the start, not 20 years later.

arc776··on Attempts to make Python fast
As the OP states:

> mutable interpreter frames, global interpreter locks, shared global state, type slots

On top of this, Python is extremely dynamic and nothing can be assured without running through the code. So this leads to needing JITs to improve performance which then give a slow start up time and increased complexity. Even with JIT, Python is just not fast thanks to the above issues and it's overall dynamism.

It can be optimised and for sure there's some impressive attempts at doing so. However I don't think pure Python will ever be considered "fast" as these things necessarily get in the way.

I highly recommend the two videos posted here that go into more detail as to why there are limits to how far optimisation can go: https://youtu.be/qCGofLIzX6g https://youtu.be/IeSu_odkI5I

arc776··on Attempts to make Python fast
Just want to say thanks for these links, very interesting so far.

A point made in the video that seems to highlight the issue:

> Just adding two numbers requires 400 lines of code.

In compiled languages, this is one instruction! Think about the cache thrashing and memory loading involved in this one operation too. How can this possibly be fixed?

Python is a great language, but I don't know if it can ever be high performance on its own.

arc776··on Attempts to make Python fast
The thing about Javascript is it's actually a very simple language. You can make a lot of guarantees and this means performance patterns can be implied.

Ultimately JS can be reduced to a very tight engine. This is not possible with Python, it's just too dynamic.

arc776··on Attempts to make Python fast
I completely agree. Everything is baked in to be slow. There is no way around it, I don't think you can write super fast interpreters like with Javascript - I might be wrong, but so far it hasn't happened.

For general use cases the performance is fine, but only thanks to the hard work of C/CPython/Cython programmers who give up Python's rich expressibility to gain this performance. It seems like you simply have to use another language to get anything running fast.

Having said all that, Pyc seems interesting as it apparently compiles Python. Has anyone had any experience of this?

arc776··on Attempts to make Python fast
I gave up trying to make Python fast since to do so you give up what makes Python good and end up writing C/Cython. On top of this, distributing Python is just... gross, at least for my use cases.

Eventually I found Nim and never looked back. Python is simply not built for speed but for productivity. Nim is built for both from the start. It's certainly lacking the ecosystem of Python, but for my use cases that doesn't matter.

arc776··on Prologue: A web framework written in Nim
Regarding GUI, there is also https://github.com/yglukhov/nimx which is based on OpenGL.

> Nimx officially supports Linux, MacOS, Windows, Android, iOS, Javascript (with Nim JS backend) and Asm.js/WebAssembly (with Nim C backend and Emscripten).

arc776··on Introduction to ARC/ORC in Nim
Another reason: compile times are super fast compared to Rust and C++ even with lots of metaprogramming.
arc776··on Introduction to ARC/ORC in Nim
Yeah I should have stressed that it's just the productivity that matches Python, not the operation. As you say it's similarity is only skin deep with significant whitespace.

In terms of use I find it feels a lot like a better Pascal but with powerful metaprogramming.

arc776··on Introduction to ARC/ORC in Nim
Nim is strongly, statically typed. It's very type safe.

Nim being able to compile to Javascript and, say, C means you can write your server and web client in the same language. This means you can share code between client and server which is particularly handy for having modules with type declarations imported by both for example, so you have a unified place to update them.

You also get to use Nim's strong type guarantees and metaprogramming when outputting Javascript. An example of why this is useful is generating say, RPC calls from a static JSON file that are automatically unified between server and client.

arc776··on Introduction to ARC/ORC in Nim
> I've had my share of burnouts because of its lack of types and difficulties in refactoring big code-bases.

Nim is strongly, statically typed. Also, I agree and aside from performance it's why I started using Nim instead of Python for my gamedev hobby. Dynamic typing isn't good for large projects. Nim has great type inference though, so you get the readability of Python but with strong compile time type guarantees.

Eg;

    type SpecialId = distinct int # This int is now type incompatible with other ints
    proc handleId(id: SpecialId) =
      # ... do something with id.
arc776··on Introduction to ARC/ORC in Nim
Whilst there is a pretty well fleshed out stdlib, since Nim can compile to C, C++, Javascript, and even LLVM, you can use any library written for those languages/platforms. That's a huge mass of ecosystems that are natively accessible.

Nim's FFI is excellent and makes this very easy without worrying about the ABI (since you're compiling to the target languages).

There's also excellent interop with Python with embedding Python within Nim or calling Nim from Python.

arc776··on Introduction to ARC/ORC in Nim
I would say one of Nim's biggest strengths is the productivity of Python with the performance of C; it feels like scripting, but you have production ready performance.

Another productivity benefit is compiling to C, C++, and Javascript means you can natively use libraries from those languages.

If you want to go deeper, check out the extremely powerful metaprogramming capabilities that rival Lisp and I would argue are much more advanced than Rust. The end result of this is very nice syntax constructions which means easy to read code, and again translates to high productivity with no performance loss.

arc776··on Introduction to ARC/ORC in Nim
> the tradeoffs of ORC and availability of manual memory management

For those not familiar with the language, Nim only uses GC for `seq` (dynamically sized lists like c++ vectors), `string`, and types declared with `ref`.

Everything else is a stack allocated value type, or as noted in the parent, you can manually manage with pointers.

> won't materially harm hard-realtime systems use-cases in practice

I think this is true. I already find myself rarely using GC when writing Nim, but when you want it, it's nice yet painless.

Here someone is embedding an async messagepack/JSON RPC on an ESP32: https://forum.nim-lang.org/t/6916

Later they compare no async, and ORC with async and find that it's "only a few ms slower than the RPC calls running with ARC and no async."

While this example may not be the hardest of realtime, it does show that ORC competes with manual approaches to memory management in constrained environments.

arc776··on NimConf 2020 Videos
Network protocol stuff, database tools, web clients & servers, games, graphics, AI, cryptography, and various random projects over the past 5-6 years. Some professionally, some for fun.

It's an incredibly adaptable tool. I mean, just take the example of writing type safe Nim code and compiling it to JS for a web client (already handy), then writing a high performance server in the same language, sharing the same types/code, but compiled to C. We can bring together front-end and backend! :)

Another generalist ability is with libraries you can get a high cooperation with Python - you can run Nim from Python and Python from Nim - so you can merge with that ecosystem too and use existing infrastructure as required. And of course an even better situation exists with C and C++.

Personally it's been a great way to diversify my ability to reach different domains with a unified tool.

arc776··on NimConf 2020 Videos
You generally want to use the simplest tool for the job first. Most of the time generics and templates really are as much as you need since Nim's generics are structurally typed and templates allow a lot of metaprogramming without getting into AST trees.

My experience debugging complex compile time transformations is pretty positive thanks to the tools available to manipulate and debug the syntax trees. For example you can `echo node.repr` to display the code it represents at any point, and going the other way you can pass code to `dumpAstGen` to display the code that will generate the AST tree and so on.

This also has the benefit of letting you quickly understand how the trees are structured by trying different bits of code, or for passing DSL syntax fragments to see the parsed tree structure for your macro to consume.

The main important thing is that stack traces at compile time get you to the place where the error is happening. How hard that is to debug depends on how complex your tree manipulation is, and how you are doing that manipulation.

For example if you're just got a load of `quote` statements that interpolate variables and you're chucking these together, the trace will usually lead to the start of the `quote` block, so if you've got a long `quote` block then you have to kind of find the error in that, but it's not too bad since the error messages are pretty precise and the language is strongly typed.

For direct AST manipulation, the error will point to the place where the error is triggered, so for example if you add a bunch of statements in a node, the error may be the place that node is added to it's parent to be 'connected' to the program as a whole. Or, if it's an attempt at a duplicate variable declaration it will point directly to the place where you're creating the `ident` that represents the variable in your macro, so that's pretty nice.

In short, yes it is more work, but it generally makes sense, is type safe, and we have tools that mean fixing stuff is often some `echo` statements to look at generated code.

arc776··on NimConf 2020 Videos
> Nim on the surface has a dream set of features

It's "too good to be true" is for real. The lack of friction when developing in Nim is what makes it so fun to code in (IMO). Metaprogramming in particular is just great in Nim and give you a lot of scope for doing things that would otherwise require new language features.

> how do you cope with the ever increasing scope of the language?

I've been using Nim pretty intensively for several years. From my perspective the language design has been very stable, even since before 1.0. I don't feel like the scope is expanding, and I don't think it really needs to either because the core language is built to be expanded with metaprogramming.

None of my code breaks when updating versions (and I do a lot of heavy metaprogramming). Nothing seems to be abandoned, only refined, and there's a big effort towards backwards compatability so breaking changes are exceedingly rare. The only one I only remember was some time ago when seq (the variable sized list type à la C++ vector) became not nil, so I had to replace `if list != nil` with `if list.len > 0`, so a good change IMO.

Most of the new things have been mechanical under the hood improvements and ownership is one of those. As I understand it, when using gc refs, you'll get free extra speed, better multithreadding, and better compile-time checks. No code needs to be changed, and when it's considered ready it will replace the previous GC, and you can add extra annotations for performance. It's refinement of the GC using ownership rules rather than a whole new set of rules for your program.

The stdlib is very stable (at least from my experience), and again great effort is spent on backwards compatability. Stdlib focus is on being small and "essential", so there's a high bar for stuff getting into it, and probably why it's rare for anything to change API there.

So personally the pace of change has been great - basically my code's just got free speed boosts when it's already fast enough :)

← PreviousPage 2 of 4Next →