Parallelizing PNG: choosing Rust for mtpng
brionv.com
brionv.com
Please give Rayon iterators another try instead of using thread pool + message passing, it's one of the best features of Rust.
Isn't it possible to implement these iterators in any language? Why are they a feature of specifically Rust?
Not that you lose that safety doing the thread pools and such manually, it’s just unnecessary.
I guess if you are iterating over something declared unsafe? But the compiler should be able to recognize that and fall back to the linear version automatically.
I'm not quite sure about the constant cost thing, but I would imagine that it's true, as it's inherently doing more work.
- not every collection/combination of iterators supports parallel work. For some it doesn't make sense, for some it's just not implemented.
- while Rust prevents data races, it can't prevent deadlocks, so you need to be mindful of locks.
- .iter() is allowed to mutate state outside of the iterator and use non-thread-safe objects, but .par_iter() isn't (without explicit synchronization primitives). So par_iter() will force you to use more expensive atomic and mutex-locked objects.
For data-bound problems most of the time it just works great. I've ran into problems with some networking libraries having global state, which prevented use of par_iter() (but no crashes - caught at compile time!)
Or rather, if you are calling something unsafe, you have to write "unsafe { }". That's a sort of "warning" in a sense. But if that unsafe code is wrapped in a safe interface, it's that code's responsibility to be correct. Bugs do happen!
You already don't need to think about it. If you're not calling unsafe yourself, then it's not your job.
That said, unsafe is not generally more convenient than safe and so people empirically don’t do this in the first place.
The compiler does tell you if it’s thread safe. That’s the point.
You can also statically link C++'s standard library, just as you do now with Rust's.
[1] https://gcc.gnu.org/onlinedocs/libstdc++/manual/license.html
> When you use GCC to compile a program, GCC may combine portions of certain GCC header files and runtime libraries with the compiled program. The purpose of this Exception is to allow compilation of non-GPL (including proprietary) programs to use, in this way, the header files and runtime libraries covered by this Exception.
See https://gcc.gnu.org/onlinedocs/libstdc++/manual/license.html
I'm definitely not experienced enough in C++ to know the details around this but I would love to hear about it from someone who is. I know some of the limitations of Rust's current type system but there is a decent amount of work underway to implement things like constant generics.
The first is what happens when you get something wrong. Let's say you write a templated function. This is Rust syntax, mapping it to C++ is left as an exercise for the reader:
fn foo<T>(x: T) {
x.bar();
}
Rust checks the types before expansion, not after. So you get this error: error[E0599]: no method named `bar` found for type `T` in the current scope
--> src/lib.rs:2:9
|
2 | x.bar();
| ^^^
In C++, this stuff is checked after expansion, so if you only pass things that have bar to foo, you're all good! It will compile. But when you pass something that doesn't, you'll get an error then.This is a restriction, but one that leads to better error messages, and stronger checks. You'd need to write
fn foo<T: Bar>(x: T)
where Bar is a trait that provides a bar method.The second difference is what is allowed in generics: Rust only lets you use type parameters. We have accepted an RFC to allow constant expressions (the most straightforward of which is 'integers'), but it hasn't been implemented yet. C++ lets you do this today https://stackoverflow.com/questions/499106/what-does-templat... and https://en.cppreference.com/w/cpp/language/template_paramete... (they also have "template template parameters" aka higher kinded types https://en.cppreference.com/w/cpp/language/template_paramete...)
It interacts in strange ways with operator overloading, namespace resolution, and other language features like constructors. There are all sorts of template helpers that are necessary but leave you scratching your head until you actually see them in use and find the corner case that they're needed for, like std::declval. There are also all the rules about ADL that interact with templates, and then the cherry on top is SFINAE which lets you control whether a problem with a template is an error or not.
Simple templates are not too hard to grok, but writing actual generic types ends up being painful due to the proliferation of corner cases you have to consider, and there are a lot of things that you could do with a macro system that are frustrating and difficult with templates, but since they're "almost possible" you end up contorting the code a little bit and sprinkling on some ad-hoc helper definitions until it works.
It's better than the C preprocessor but worse than almost everything else.
Languages like Lisp have proper macros, so anything you want to do with a template you can just achieve by defining it in a macro... you have more rope to shoot yourself in the foot with, but the rules for Lisp macros are straightforward and easier to understand (you just write a function that spits out code, basically).
Languages like C#, Java, Haskell, etc. have better generic types, so you can do something like say "this function takes an array of type T[], and T must implement the Widget interface, it will give you back a single T". In C++ you can accomplish this goal but you can't as easily encode the constraint that "T must be a Widget", you have to do something weird like put a static assert in the function body, or put a std::enable_if in the type, or accept that there will be template substitution errors with possibly long and difficult-to-read error messages.
Mind you, this has really underscored (for me) the amazing developments in our understanding of language design over the past decades. C has preprocessor macros which are barely serviceable and impossible to use for anything complicated, C++ has templates which are more powerful but complicated, Java generics with type erasure are very clean but interact in subtle ways with e.g. covariance and contravariance, C# addresses the covariance and contravariance problems but interacts poorly with operator overloading. TBH I think Go made the right decision to keep both generics and operator overloading out of the core language at the beginning, since not having it at all is in many ways preferable to having a bad version.
However, in practice you often avoid boxing and unboxing in C#. This is due to the differences between the type system for C# and Java, with Java generics are built with type erasure which forces you to box and unbox everything. With C#, internally a List<T> will have a T[] inside it, and if T is a struct type you will not need to box or unbox it.
I'm not sure what examples you are thinking of where you are paying for boxing and unboxing due to generics in C#, I guess I haven't run into that particular problem. In general, generics in C# avoid boxing and unboxing. That's the difference between System.Collections (which doesn't use generics, and requires boxing/unboxing) and System.Collections.Generic (which does use generics, and as a direct consequence, avoids boxing/unboxing).
In my mind, "specialization" means ad-hoc polymorphism. That is, you can change the behavior of a generic function, depending on its instantiated types. C# does not have this.
However, that does not mean that you get the same machine code for both object and reference types. As an optimization, when you use a template in C#, code is generated for the instance you are actually using. For value types, this means that the code will not box or unbox. This is clearly not specialization, at least by the definition I'm using, but it also avoids unboxing.
> The interesting question is how does .NET compile the generic IL of the server to machine code. It turns out that the actual machine code produced depends on whether the specified types are value or reference type. If the client specifies a value type, then the JIT compiler replaces the generic type parameters in the IL with the specific value type, and compiles it to native code. However, the JIT compiler keeps track of type-specific server code it already generated. If the JIT compiler is asked to compile the generic server with a value type it has already compiled to machine code, it simply returns a reference to that server code. Because the JIT compiler uses the same value-type-specific server code in all further encounters, there is no code bloating.
from https://msdn.microsoft.com/en-us/library/ms379564(v=vs.80).a...
Also this gem,
> Because the generic code does not force the boxing and unboxing of value types, or the down casting of reference types, performance is greatly improved.
Not interested by his implementation strategy but would only recommend this get merged into libpng to make the largest impact.
Over the years there've been wildly different PNG file sizes And a good encoder can secure something like 50% file size against a naïve implementation.
However,
* gzipping 7680×2160x3 bytes from /dev/urandom on my thinkpad x220t takes 1.6 seconds (not mutch difference between -1 and -9)
* gzipping /dev/zero takes 0.4 seconds
So it turns out this might be a problem after all!
Random data doesn't compress well, and requires a lot more effort by the compressor.
Images aren't anywhere near that random.
In a video game or real-time mpeg stream decoding job, the video card has to generate this amount of data 30-60 times per seconds. So 0.4-1.6 seconds to compress the result seems long in comparison.
I tested the 2 extreme situations I could think of: ultimate random and ultimate order. Images will fall somewhere between these 2 limits, so on my PC, the PNG compression has a lower bound of 0.4-1.6 seconds to write an image with the current algorithm.