256 karma · joined January 28, 2016
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.htmlThere'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.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
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.
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.
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.
> 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.
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.
> 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.
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?
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.
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.
> 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
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.
Ultimately JS can be reduced to a very tight engine. This is not possible with Python, it's just too dynamic.
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?
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.
> Nimx officially supports Linux, MacOS, Windows, Android, iOS, Javascript (with Nim JS backend) and Asm.js/WebAssembly (with Nim C backend and Emscripten).
In terms of use I find it feels a lot like a better Pascal but with powerful metaprogramming.
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.
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.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.
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.
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.
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.
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.
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 :)