PyPy - We need Software Transactional Memory
morepypy.blogspot.com
morepypy.blogspot.com
* Clojure's data structures are purely functional, and built with the STM in mind. Updates and rollbacks to structures are cheap.
* Clojure user-level code has always had access to the STM. Because that's the accepted paradigm, There are an order of magnitude fewer assignments in typical clojure code, greatly simplifying the problem. In typical clojure, one function that uses the STM will call 10 pure functions. In typical python, most of those 10 calls will all mutate local state.
* Clojure requires all STM activity to happen within the dosync block. In python, that would be like requiring the magic to happen within a "with transaction(): ...". Again, this limits the amount of work that needs to happen. The compiler doesn't need to decide whether or not a random function needs to be a transaction; the user will tell the compiler.
* Clojure's STM explicitly only works with functional code. This will likely cause fewer leaky abstractions going forward. PyPy will have to modify all existing data structure classes to work with the new STM, and they won't be able to fix all the library and user code out in the world.
Good luck to them, but I suspect bolting STM into an existing language is much harder than designing it from scratch, similar to building in GC.
STM in Haskell is very natural, since Haskell already enforces pure code and side-effecting code. STM and STVar work pretty similarly to IO and IORef or ST and TVar.
How does Clojure enforce that there are no side-effects in the transactional code?
Technically it doesn't. If you have a (println "foo") in a transaction it may be
printed multiple times if the transaction gets retried.Clojure does give you an io! macro that you can wrap all your side effects in to manually provide that kind of protection. If you try to use io! inside a transaction you'll get an error at runtime.
user=> (defn foo [n]
(io!
(println n))
n)
#'user/foo
user=> (defn bar [] (dosync (foo 10)))
#'user/bar
user=> (bar)
java.lang.IllegalStateException: I/O in transaction (NO_SOURCE_FILE:0)> Clojure requires all STM activity to happen within the dosync block. In python, that would be like requiring the magic to happen within a "with transaction(): ...".
Under the proposal, all Python code would run under STM, only C-API and OS-level codes would run outside STM.
Edit: to clarify, the most common issue with implementing a viable STM (in just about every language apart from haskell) is the interaction between STM-managed objects and non-stm-managed objects. Under Pypy's proposal, there would be no such thing as all objects would be STM-managed.
Under the current pypy proposal, they're going to STM-ize every function call, regardless of whether it needs to do STM work. If the language had been built with STM in mind, with an explicit STM call, more user and library level code would adapt to that, and use STM sparingly.
The fact that only place it has been shown to work is basically Clojure and Haskell needs to be addressed; how are you going to address the apparent fundamental incompatibility between imperative languages and STM? And while you may be able to answer that at the VM level (very tiny transactions implemented between opcodes, basically), I would expect it is absolutely hopeless to allow users access to this. How are you going to address the endless-side-effect replay problem? (Hint: You aren't.)
This is a kill-PyPy level of dangerous decision, and I think everyone on that team ought to demand a very good answer to that question before buying into this.
From a PyPy as a research project, I'm excited. I'm skeptical of STM as a synchronization solution as well, but I still want us to explore it. If their approach truly is novel, then maybe there's something there.
It would not be the first time they tried implementing something and failed either.
Sure, if it's some guy's branch, hey, whatever, but I'm running on the assumption that this is being promoted not as SomeGuy'sBranch but as an argument about the way the official project should go, because otherwise, why proselytize when you can just do? An awful lot of projects have died this way.
The problem is, this won't die in week two. It'll only die after a lot of effort and either several stalled or buggy releases. It's not an "it just doesn't work, crashes the system" problem, it's an infinite regress problem.
As Armin mentions in the blog post, the STM can be explored at the transformation/translation level. It seems to me the level of commitment here is more "interesting branch that through trial and measurement may lead to a important language-level breakthrough" rather than "stone albatross that drowns the project."
Yes it will impose a tax on all mutable data structures, and push people towards a more functional style of programming. This is generally good. It may be a challenge for people who think solely in terms of OOP, but then OOP will evolve in response, and that is also good.
[1] http://en.wikipedia.org/wiki/Multiversion_concurrency_contro...
If PyPy can break the GIL, I would anticipate that we'd seriously consider pushing our users toward it.
Atomic.Do(() => { /* atomic code */ })
Here's some channel 9 videos about their progress, which are in of themselves very interesting:http://channel9.msdn.com/Shows/Going+Deep/Software-Transacti...
http://channel9.msdn.com/Blogs/Charles/STMNET-Who-What-Why
And here are some research papers published by Tim Harris and the old STM research blog:
http://research.microsoft.com/en-us/um/people/tharris/
http://blogs.msdn.com/b/stmteam/archive/2010/05/12/stm-net-d...
Fascinating stuff. I'm wary of having it be at the core of Python, but very interested to see where PyPy ends up.
Nitpick: unless the field in questions is a "long" or "double". In those cases the operation becomes two bytecodes and you can in fact see something else entirely if you are unlucky. At least it used to be like that.
On 64-bit, you won't. =)
It will likely be difficult to beat expertly written code using explicit locks. But most people aren't experts in concurrency and will either get it wrong or have slow implementations. And if transactional memory catches on, we may even see some hardware assistance in future CPUs.
(S)TM is definitely worth exploring more and even a 2x slower implementation (as envisioned by the PyPy team) could cover most concurrency needs, which will make it a success in most people's eyes.
I think transactional memory systems with at least some hardware support are potentially more interesting.
What comes to the performance, the benchmarks in the paper range from no speedup at all with 8 cores to having 1.5x perf increase with 2 cores to 4x increase with 8 cores. To me, that definately sounds like something worth researching.
codedivine's point was that it is often the case that applications implemented with STM that use multiple threads often perform worse than the sequential version. See the excellent ACM Queue article he links to in a sibling comment.
And the experience has been exactly the same in locks-based GIL-removal tests in Python: David Beazley recently "unearthed" and tested a GIL-removal patch from the 1.4 days[0]...
> To test threads, I wrote a small sample that subdivided the work across two worker threads is an embarrassingly parallel manner (note: this code is a little wonky due to the fact that Python-1.4 doesn't implement thread joining--meaning that you have to do it yourself with the included binary-semaphore lock).
> [...]
> If you run this code with the GIL, the execution time is about 2.5 seconds or approximately 1.3 times slower than the single-threaded version (1.9 seconds). Using the GIL-less Python, the execution time is 18.5 seconds or approximately 1.45 times slower than the single-threaded version (12.7 seconds). Just to emphasize, the GIL-less Python running with two-threads is running more than 7 times slower than the version with a GIL.
[0] http://dabeaz.blogspot.com/2011/08/inside-look-at-gil-remova...
That makes me think that an implementation of Python that uses fine-grained (or at least finer grained) locks that scales is possible. It's just a question of how feasible is it to transform CPython into such an implementation. Beazley's experiment indicates that the changes may have to be fundamental, such as moving away from reference counting.
I'm pretty excited. The chip modifications here are really small and cheap, so I think we may start seeing this in more mainstream server chips in a few years.
Thats all the rage these days anyways as its easier to reason about. Better still it matches distributed computing models much better.
If they go the STM route and spend years hashing out the details only to be 2x as slow as CPython, then that cuts out a huge number of people who would like to practically use PyPy.
Message-passing, on the other hand, doesn't need particular locking and crashes are (somewhat) simpler to deal with.
(And most any scheme I've encountered can deadlock, and will usually choose the most inopportune moment to demonstrate this capability.)
There's an older blog post here http://enfranchisedmind.com/blog/posts/thoughts-on-paralleli... that's worth a read.
With this in mind, it is possible to imagine a whole-
program transformation that would add locking on every
object manipulated in RPython by the interpreter. This
would end up in a situation similar to Jython. However,
it would not automatically solve the issue of deadlocks,
which is avoided in the case of Jython by careful manual
placement of the locks. (In fact, being deadlock-free is
a global program property that cannot be automatically
ensured or verified; any change to Jython can in theory
break this property, and thus introduce subtle deadlocks.
The same applies to non-atomicity.)
This is just not true. It is true for the general case, but not for this specific case. Remember the pithy solution to the dining philosophers (number all the chopsticks and pick up the lowest numbered one first)?I don't know the details of the pypy interpreter, but it does seem that given a byte-code that affects several objects, you could just acquire the locks for the objects as ordered by the objects address.
Now the pypy guys are very smart and this is a simple solution, so I'm sure it yields horrible performance, but the fact of the matter is it's not true that "being deadlock-free is a global program property that cannot be automatically ensured" (it is true that it can't be verified, but if you generate all the locking code it can be ensured by strict ordering) and to say otherwise is wrong.
That is, he's talking about situations where they don't have full knowledge when entering a critical section which objects will be accessed. Hence, they can't order lock acquisition in such a way to avoid deadlock.
Fundamentally, it doesn't make a difference whether you analyse the python or the bytecode; further, I'm not even sure that RPython is ever converted to bytecode, so it's not entirely relevant here.
Pypy using STM should not change the semantics of the Python language or require alterations to code which does not rely on implementation details (code which relies on the GIL for its thread-safety may have to be fixed, it already has to be fixed for Jython compatibility).
> Isn't this harder in Python than in Haskell or Ocaml?
I'd expect STM to be much harder in imperative languages than in functional languages indeed. In fact, I don't think I've heard of a successful STM implementation in imperative languages yet.
On the other hand, I think most tentatives so far have been with STM as a library applied explicitly, and you run into issues of (mutable) objects "crossing" the transaction barrier in imperative languages, issue which "does not" happen in Haskell (there are ways to subvert the type system, but then you're on your own. In normal uses, I think objects under STM live in a different monad, and therefore can't be freely mixed with objects living outside of it) (I believe this issue would happen in OCaml, so on that front OCaml would be little better than imperative languages). This issue should not exist if all of the code runs under STM (ignoring explicit hooks to do so built into the system), and this should allow for "easy" implementation of explicit memory transactions as well (one of STM's promises is composability, so you should be able to compose multiple bytecode-transactions in a single explicit transaction).
http://www.bluebytesoftware.com/blog/2010/01/03/ABriefRetros...
Wouldn't it be enough to have a mutex for each object and when such a builtin function is called, all objects which are passed as parameters (including and esp. `self`) are locked?
Not to mention that if every object has a mutex associated, the performance will be horrible. Even if the mutexes are not contended, there will be so many atomic compare and swap operations that the interprocessor memory bus will simply die.
If your solution would work, every method in Java would be synchronized by default.
But I think you can add such a behavior in an automatic way.
However, I claim that you can also do it in an automatic way and avoid deadlocks at the same time.
E.g., like this: https://github.com/albertz/automatic_object_locking/blob/mas...