[HN Gopher] JIT-optimized Ruby can outperform a C extension
___________________________________________________________________
JIT-optimized Ruby can outperform a C extension
Author : mooreds
Score : 156 points
Date : 2023-09-07 17:43 UTC (2 days ago)
(HTM) web link (railsatscale.com)
(TXT) w3m dump (railsatscale.com)
| eutropia wrote:
| The point here is simplifying the codebase by removing an extra
| language.
|
| Rubyists like writing ruby, not C extensions. And using the JIT
| allows us to simplify and keep enough performance.
| rolae wrote:
| People say the title of the article "Ruby Outperforms C: Breaking
| the Catch-22" is misleading, which is true, this is about Ruby
| code optimized by JIT outperforming a extension written in C.
|
| But to give some context: the author Aaron Patterson is a Ruby
| and a Rails core team member. The article and headline is clearly
| targeting the ruby community, where this article has been very
| well received. I think it's a good title for the intended
| audience.
|
| The post clarifies in the first section:
|
| > In this post I'd like to present one data point in favor of
| maintaining a pure Ruby codebase, and then discuss some
| challenges and downsides of writing native extensions. Finally
| we'll look at YJIT optimizations and why they don't work as well
| with native code in the mix.
|
| edit: added original title of the hackernews post / article
| wonnage wrote:
| This is specifically about breaking the myth that performing
| expensive self-contained operations (e.g, parsing GraphQL) in a
| native extension (C, Rust, etc.) is always faster than the
| interpreted language.
|
| The JS ecosystem has the same problem, people think rewriting
| everything in Rust will be a magic fix. In practice, there's
| always the problem highlighted in the post (transitioning is
| expensive, causes optimization bailouts), as well as the cost
| of actually getting the results back into Node-land. This is
| why SWC abandoned the JS API for writing plugins - constantly
| bouncing back and forth while traversing AST nodes was even
| slower than Babel (e.g https://github.com/swc-
| project/swc/issues/1392#issuecomment-...)
| vidarh wrote:
| I think a parser is perhaps _the_ example of where resorting
| to a compiled extension can be beaten by something more JIT-
| favourable.
|
| In a language like Ruby it tends to be heavily dominated by
| scanning text, and creation of objects, and 1) you can often
| speed it up drastically by reducing object creation. E.g.
| here is Aaron writing about speeding up the GraphQL parser
| partly by doing that[1], 2) creating Ruby objects and
| building up complex structures in the C extension is going to
| be almost exactly as slow in the C extension as in the Ruby,
| 3) the scanning of the text mostly hits the regexp engine
| which is already written in C.
|
| (That said, I heavily favour not resorting to C-extensions
| unless you _really_ have to; even without going as far as
| some of Aaron 's more esoteric tricks for that parser you can
| often get a whole lot closer that you think, and the portion
| you need to rewrite if you still have to might well turn out
| to be much smaller than you'd expect)
|
| [1] https://tenderlovemaking.com/2023/09/02/fast-tokenizers-
| with...
| mananaysiempre wrote:
| Interesting that both of the points you state completely
| contradict my experience with LuaJIT.
|
| Parsing has always been one of the things its tracing JIT
| struggled with; it is still faster than the (already fairly
| fast) interpreter, but in this kind of branch- and
| allocation-heavy code it gets nowhere near the famed 1.25x to
| 1.5x of GCC (or so) that you can get by carefully tailoring
| inner-loopy code.
|
| (But a tracing JIT like LuaJIT is a different from a BBV JIT
| like YJIT, even if I haven't yet grokked the latter.)
|
| LuaJIT's FFI calls, on the other hand, are very very fast.
| They are still slower than _not_ going through the boundary
| at all, naturally, but that's about it. On the other hand,
| going through the Lua /C API inherited from the original,
| interpreted implementation--which sounds similar to what the
| Ruby blog post is comparing pure-Ruby code to-- _can_ be
| quite slow.
|
| The SWC situation I can't understand quickly, but apart from
| the WASM overhead it sounds to me like they have a syntax
| tree that the JS plugin side really wants to be GCed in the
| GC's memory but the Rust-on-WASM host side really wants to be
| refcounted in WASM memory, and that is indeed not a good
| situation to be in. It took a decade or more for DOM
| manipulation in JS to not suck, and there the native-code
| side was operating with deep (and unsafe) hooks into the VM
| and GC infrastructure as opposed to the WASM straitjacket.
| Hopefully it'll become easier when the WASM GC proposal
| finally materializes and people figure out how to make Rust
| target it.
|
| In any case, it annoys me how hard it is in just about any
| low-level language to cheaply integrate with a GC. Getting a
| stack map out of a compiler in order to know where the
| references to GC-land are and when they are alive is like
| pulling teeth. I don't think it should be that way.
| aardvark179 wrote:
| There is a very big difference between a simple FFI system
| and the sort of C interface offered by Ruby and Node. Those
| interfaces allow objects to be passed to the native code,
| and the native can then do pretty much anything to the
| language run state. This is great if you want a C library
| that can do anything your higher level language could do,
| but it also means the JIT has to treat all those calls as
| impenetrable barriers that cannot be optimised through, so
| even a small C call can prevent the rest of your
| application from being optimised.
|
| We got round this in TruffleRuby by running C extensions
| through an LLVM Bitcode interpreter that was part of the
| same framework as the Ruby interpreter and allowed them to
| be JITted together, but that had other downsides, and
| wasn't great for things like parsers which had huge switch
| statements.
| mike_hearn wrote:
| Yes but in this case the TruffleRuby approach would fix
| the Shopify issue, I think? And if by downside you mean
| longer warmup times that's an issue for YJIT or any other
| JIT too, so how much of a downside it is depends a lot on
| the nature of the deployment.
| byroot wrote:
| Shopify bigger repos are deployed pretty much every 30
| minutes. As you point out most JITs struggle in these
| conditions.
|
| But YJIT warms up extremely fast, and is able to provide
| real world speedup to these services almost immediately.
| danmur wrote:
| I think 'magic fix' could be replaced with 'fun thing to do'
| yebyen wrote:
| I spent a while rewriting a tiny bit of some useful Ruby in
| Rust and integrating it via Wasmer, and I definitely think
| my time expenditure is better classified as "fun thing to
| do" than "magic fix".
|
| https://ossna2023.sched.com/event/1K55z/exotic-runtime-
| targe...
|
| https://www.youtube.com/watch?v=EsAuJmHYWgI
|
| My goal was to determine: how will I use Wasm as a Rubyist?
| Spoiler: I did not intend to use Rust, but embedding Ruby
| in a Wasm and running it from within Ruby proved to be a
| fool's exercise. I have a feeling that some of the claims I
| made in this talk about there being "no theoretical
| benefit" to running Ruby in Wasm in Ruby will quickly be
| proven incorrect. But I'm not expecting it to be faster.
|
| If it's ever faster, that will be filed under "surprising
| results"
|
| Michael Yuan who obviously knows a lot more about Wasm than
| I do addressed this in his talk as well, but not from the
| perspective of a JIT necessarily, although I guess you
| could consider local machine compile target optimization on
| the target machine at runtime as a type of "just in time
| optimization" it's not really (it's just the regular ahead-
| of-time type of optimization, but not fumbled and the
| majority of benefits immediately getting lost in such a way
| as it usually is...)
|
| https://www.youtube.com/watch?v=kOvoBEg4-N4
|
| The spoiler from that talk (for me anyway) was finding out
| that you do see surprising results sometimes, and sometimes
| it's due to a pathological case that (a) happens all the
| time, and (b) does not have a readily obvious solution in
| the form the problem regularly takes. Like linux distros
| shipping generic binaries "target-optimized" for the lowest
| common denominator on any given particular architecture
| dist target, because the binaries they ship obviously have
| to run anywhere.
|
| (Recap of the conversation we had off-stage after the Q&A
| ended: We don't know when we say "x86-64" what modern
| opcode targets that means we really have available, so we
| have to ship a binary with only the oldest opcodes that are
| guaranteed to be available on any similar chip that we
| intend to support. Web Assembly uses a compiler on the
| target machine, cranelift, to translate from a platform
| independent binary to a platform-specific one. Running the
| compiler takes a while, but we can do it ahead of time.
| Will we come out ahead in the end?)
|
| All of this stuff is certainly very fun to reason about :D
| pmontra wrote:
| TL;DR
|
| This is about pure Ruby code optimized by JIT vs manual
| optimization by writing C extensions (native code). There is Ruby
| code around those extensions but
|
| > The JIT cannot know what the native code will do to parameters
| (possibly mutate them), it cannot know what type of Ruby object
| will be returned, if the native code will look at the stack, or
| if it will raise an exception, etc. Thus the JIT is forced to
| have a conservative approach when dealing with native code,
| essentially forgetting much of what it had learned, and falling
| back to the most conservative implementation possible.
|
| Hence the JIT often makes pure Ruby code run faster than the code
| with C extensions.
|
| I think I heard the same argument many times with other JITed
| languages, interpreted or compiled.
| [deleted]
| xxs wrote:
| Any managed language - java and go included, pays for trips to
| external, non-intrinsics calls. The standard way to deal is
| large portions/buffers to amortize the costs. This has been the
| case since the late '90s
| andrewstuart wrote:
| "X outperforms C/C++/Rust/Golang".
|
| Only if the code design in C/C++/Rust/Golang is bad.
| [deleted]
| josephg wrote:
| (Edit: my mistake, I misread the comment I was replying to
| assuming it claimed these languages themselves were
| equivalent.)
|
| Why would the performance of these languages be identical?
| These languages aren't the same in lots of ways.
|
| Golang has a GC and gives you much less control over memory
| layout than any of the other languages you mentioned. It also
| has a very different concurrency model, which will vastly
| change the performance characteristics of multithreaded code.
|
| Rust is limited to llvm, while C can be compiled with many
| other compilers - some of which produce better code in many
| cases. (Eg icc)
|
| Rust has much better noalias analysis - which allows the
| compiler to make optimizations that aren't available in general
| purpose C or C++ code. (Since nobody adds noalias to all their
| variables in C, for a variety of reasons).
|
| Unlike the others, C doesn't support monomorphization (without
| disgusting macro tricks). Monomorphization cuts both ways with
| performance - since it bloats executables (and thus hurts the
| branch predictor and cache) while generating more efficient
| code.
|
| These languages aren't the same. There's lots of reasons they
| will perform differently both at the limits of what we can
| optimize and in the average case of everyday code. It's wild
| that you would consider them to all be the same.
| PartiallyTyped wrote:
| > Rust is limited to llvm, while C can be compiled with many
| other compilers - some of which produce better code in many
| cases. (Eg icc)
|
| There's a rust front-end for gcc as well.
| josephg wrote:
| Is it ready for use? Last I saw it was far from feature
| complete.
| PartiallyTyped wrote:
| Afaik the borrow-checker is missing - please correct me
| if I am wrong - but that doesn't stop you from using it.
| You could very well use `rustc` for the borrow-checker
| and compile with GCC on any other backend. The borrow-
| checker only expresses that your code is sound according
| the rust spec.
| josephg wrote:
| > The borrow-checker only expresses that your code is
| sound according the rust spec.
|
| Doesn't the borrow checker also track when things fall
| out of scope, and thus control when Drop is called?
| PartiallyTyped wrote:
| Afaik it tracks when objects go out of scope as that
| deals with ownership and lifetimes, but that's also part
| of the rust spec.
|
| The drop is there for stuff that you can't just release
| the memory for, such as closing files and connections.
| iudqnolq wrote:
| No. Drop has straightforward rules.
|
| Find the scope you can refer to the variable in, i.e. the
| curly brackets it is contained in. That variable is
| dropped right before the closing curly bracket. Things
| are dropped in reverse order of creation.
|
| (Similar rules apply to dropping temporaries)
|
| https://doc.rust-lang.org/reference/destructors.html
| nvy wrote:
| For the time being. But C on x86 hasn't been a good model of
| what the CPU is _actually doing_ for a while now and eventually
| we 'll have a better way to harness the computing power
| available to us.
| throwaway17_17 wrote:
| I want to be optimistic and agree that 'we'll have a better
| way to harness the computing power available to us', but at
| this point it's been years or maybe decades of propping up
| the x86 ISA while actual operation of the computing device
| drifts further away. I certainly understand the desires of
| processor manufacturers to want to be able to maintain an ISA
| and then be able to implement a machine that can change
| underneath it to realize performance gains. However, this has
| left the status quo utterly entrenched and I can not see what
| would make them change this behavior.
|
| I really would get onboard with an alternative ISA that was
| more in line with actual architectural features of modern
| processors and I often wonder why we could not have an extra
| compilation (jit or aot) step from x86 to some other format.
| But the trend seems to be going the opposite direction for
| all the large scale processor manufacturer/designers toward a
| more abstracted interface at the lowest level.
|
| I've seen various comments that basically boil down to "you
| can't change the ISA because then C/C++/Rust programs would
| loose performance in benchmarks in the short term", and this
| resistance only reinforces maintaining current trends. So...
| what are you assuming happens that prompts the shift toward a
| better way to harness computing power?
| anonymoushn wrote:
| What architectural features would you like to surface that
| are currently papered over in x86?
| throwaway17_17 wrote:
| My particular interests would see me appreciating access
| to cache structure, cache behavior, prefetching, cache
| coherency, memory management unit control, instruction
| pipelining, and maybe register renaming control.
|
| I am not particularly qualified to try and design a
| better ISA, I just know there are some things I would
| like the option to control at some times (or at least I
| imagine I would). A lot of the list above is about
| exposing more of the architecture to user control as a
| general principle. However, I'd rather see some areas of
| hardware design change to facilitate something other than
| coding paradigms and practices that stretch back nearly
| 40 years. Chisnall's 'C is not a Low Level Language'
| article discusses several architectural directions I
| would like to see happen, but also talks about how as
| long as the ISA remains the same moving to new
| programming styles/philosophies continues to be a
| difficult proposition.
| anonymoushn wrote:
| I see. These things sound swell, though for stuff like
| controlling register renaming it sounds hard for the
| instruction decode overhead to be worth it. Currently you
| can use prefetch intrinsics for prefetching and the level
| of cache management available is mostly non-temporal
| stores and explicit allocation of L3 space to specific
| cores (which ends up meaning specific processes if you
| are careful).
|
| The world has mostly not adopted existing tools for
| writing 10-100x faster software (simd, huge pages,
| computing both branches then using a blend or cmov when
| this is sufficiently cheap, software prefetching,
| collection APIs that support bulk updates so that they
| can use all of these things instead of adding totally
| unnecessary dependency chains to the resulting program,
| etc.) so i'm not optimistic about things getting better
| if CPUs grow a thousand times more memory bandwidth and
| cores and begin to support very wide scatter/gather like
| GPUs.
| emodendroket wrote:
| ARM seems like it's destined to only grow in importance.
| throwaway17_17 wrote:
| I don't know for certain, or in any great specificity,
| but in this context (i.e. ISA representing a more
| accurate or concrete mapping to hardware
| functionality/capabilities) I don't know that ARM is
| particularly different to x86. The both present a
| relatively single core, flat memory architecture to
| programmers; while the actual processor is absolutely not
| that.
| xxs wrote:
| The memory model is a bit different on arm64, but it is
| not Alpha.
|
| Other than that they are very similar
| emodendroket wrote:
| You may be right about that, though I had the impression
| that less wasteful parallelism was possible because of
| fixed-size instructions.
| anonymoushn wrote:
| immintrin.h seems tough to beat.
| mgaunard wrote:
| C is a pretty good model in part because all ISAs were
| designed with C in mind.
| starbugs wrote:
| C was never intended to be a model of what the CPU is
| actually doing.
|
| This is purely an optimization issue and the article compares
| apples to oranges for clicks.
| anonymoushn wrote:
| Golang's compiler is not great. The main way to achieve
| acceptable performance in golang is to use their weird asm
| dialect.
| josephg wrote:
| As well as differences in code gen, as I understand it Go
| also has worse tail latencies than servers written in
| C/C++/Rust because of the GC.
| Patrickmi wrote:
| In this context, they are not talking about GC but the
| compiler
| umvi wrote:
| Golang's compiler is so fast though that I can iterate much
| faster so I'm willing to accept slightly worse performance
| than C++. C++ I waste so much time just waiting (esp. if I'm
| using a lot of templatized header only libraries)
| john_the_writer wrote:
| But by that logic ruby is faster still since the compile
| time is 0. Less than zero really because it can be modified
| in flight.
| Mawr wrote:
| Only if the compilation speed difference is worth the
| loss of performance. That seems unlikely for most use
| cases because the speed gain is small while the
| performance loss is severe.
| fredrikholm wrote:
| The trade offs between the two are huge.
|
| Go programs compile in less than a second, or several
| seconds for huge projects, where idiomatic Go gets you
| within the same order of magnitude to that of C(++), with
| some memory overhead. Concurrency is first class, GC
| pauses exist but are seldom an issue.
|
| Idiomatic Ruby runs several orders of magnitude slower,
| with a huge memory overhead, and is rife with runtime
| gymnastics. Concurrency is an expensive hack, with stop-
| the-world GC.
| Kiuhrly1 wrote:
| Somehow I can't find a source for this but it's well known
| that Go prefers compilation speed over runtime performance.
| Doesn't help that they implement the entire compiler stack
| themselves so it definitely misses optimisations that other
| compilers can already do.
|
| Also, I've never heard of anyone using assembly for
| performance optimisations. It would never happen where Go in
| commonly used (server programming), maybe if you're doing
| graphics or something nonstandard?
| john_the_writer wrote:
| I've done it for a c++ parser. I grabbed the compiled c++
| and disassembled it into assembly, then reworked it. Got a
| massive increase in speed when I tweeked the registry use.
| anta40 wrote:
| Perhaps gcc-go is better?
| Patrickmi wrote:
| gcc-go, gollvm there's actually no difference in comparison
| to the go compiler, heck in some cases gcc-go perform
| worse, Go internals are built with a different mindset and
| architecture there're certain features that's can perform
| worse than just hard coded assembly
| Patrickmi wrote:
| If this was 4 years ago I would say "fair enough" but now Go
| 1.21 ? This claim is no longer Valid, just like saying CGO is
| slow, just because one language uses LLVM or GCC doesn't mean
| they have state of the compiler
| intelVISA wrote:
| Yeah it's not fair to put Go in the same bracket as fast
| langs tbh.
|
| It's a lang to ship MVPs quickly without much thought &
| cheaper devs - not a high perf one.
| Mawr wrote:
| Not at all. The main way is to reduce pressure on the GC by
| reducing allocations, which is a fairly straightforward
| process thanks to good support from the tooling.
|
| Past that, you either call out to C via FFI or give up and
| rewrite in C/C++/Rust. Nobody really bothers with the asm.
| starbugs wrote:
| I think this is a somewhat misleading title.
|
| It does not outperform C.
|
| This article appears to compare a suboptimal native C
| implementation to a JIT optimized version of some similar Ruby
| code. The JIT only outperforms that particular hand written C
| implementation in this case.
|
| Still, great that the Riby JIT can do this now, but the title
| would be better phrased as 'Ruby JIT in some cases outperforms
| suboptimal hand written C code'. That wouldn't be good clickbait
| though.
| fasterik wrote:
| Yeah, it appears that in the original benchmark an off the
| shelf C parser outperformed an off the shelf Ruby parser. Then
| they wrote their own parser in Ruby, which was faster. But for
| a fair comparison, you should also write the same parser in C
| and compare it to that.
|
| It's possible that writing it in Ruby is preferable if it
| avoids FFI overhead and can do more aggressive inlining. But
| this doesn't seem like an apples to apples comparison.
| [deleted]
| chucke wrote:
| This article outlines well the paradox that JITs require to be
| truly more efficient: if more of the target language is available
| to optimize, it'll get waaay more optimized, compared to dropping
| down to the layer below and try to hand-stitch it.
|
| Of course, there is massive overhead in doing so. Just look at
| go, which had to rewrite practically everything already available
| in go, and must always require a native implementation (protobuf
| for example shares the underlying interface across ruby, python,
| php... but then has a full separate implementation in go, and
| java I think). And they have the budget for it at least, Google
| won't let go die under the overhead it created for itself.
|
| So definitely, write more ruby, enough of those "fast-C gem -
| rewritten as C extension", but still keep using low level
| libraries like libpq.
| Mawr wrote:
| > Of course, there is massive overhead in doing so.
|
| Some concrete numbers for Go:
|
| * Calling C from Go: ~40-50ns [1] [2]
|
| * Calling Go from C: ~100-200ns [3]
|
| [1] "Cgo calls take about 40ns, about the same time
| encoding/json takes to parse a single digit integer. On my 20
| core machine Cgo call performance scales with core count up to
| about 16 cores, after which some known contention issues slow
| things down." (https://shane.ai/posts/cgo-performance-in-
| go1.21)
|
| [2] "In response to the "cgo is slow" questions, this shows
| that cgo calls are about 50ns on my four-year-old x86 MacBook
| Pro. Is that fast or slow? It depends on what the cgo call is
| doing. If it's executing a single add instruction, an extra
| 50ns is slow. If it's doing something more substantial, an
| extra 50ns may be nothing at all." - rsc
| (https://news.ycombinator.com/item?id=36006347)
|
| [3] "Calls from C to Go on threads created in C require some
| setup to prepare for Go execution. On Unix platforms, this
| setup is now preserved across multiple calls from the same
| thread. This significantly reduces the overhead of subsequent C
| to Go calls from ~1-3 microseconds per call to ~100-200
| nanoseconds per call." (https://go.dev/doc/go1.21)
| vidarh wrote:
| I'm just toying with a Ruby terminal talking direct to X (no X
| lib) running a pure Ruby TrueType renderer (and running a pure-
| Ruby editor in it...). Ruby is still not "fast" (but I haven't
| tested it with yjit yet). I put up with it because I know I can
| make it significantly faster later (and the quick and dirty
| first approximation is memoize what I can, like glyph shapes)
|
| As it turns out for a whole lot of things Ruby is slow for
| people because one of the nice things about programming in Ruby
| a lot of the time is expressing the problem the more readable
| way first rather than paying attention to performance. When
| people are aware, it makes it easy to iterate fast and fix
| performance later. The problem is a lot of the time that leads
| to pathological cases of people not thinking this through at
| all.
|
| You'll notice most of his performance increase came from
| writing better Ruby. Clearly the original Graphql parser was
| nowhere near optimal. It's worth keeping in mind that writing a
| C extension ought to be a last resort after making the Ruby as
| fast as you can first, because often that means you don't need
| to.
|
| E.g. another of Aaron's recent articles (EDIT: [1]) is about
| speeding up a parser by cutting down on object creation, and
| one specific example he gave was to return just the token type
| from the lexer instead of [token_type, token_value]. The latter
| forced creation of an Array object to hold each token and a
| string object for the token value (though for fixed tokens you
| could avoid that by returning the same frozen string literal),
| and for a whole lot of tokens the parser had no interest in the
| token string (e.g. if you see :lparen, or :rparen, getting "("
| and ")" is entirely uninteresting). When people run into slow
| Ruby code like that it's tempting to resort to C right away,
| rather than understand _why_ their Ruby is slow.
|
| I love that yjit makes it _easier_ to get to a point where you
| don 't need to reach for C, though.
|
| EDIT: [1] https://tenderlovemaking.com/2023/09/02/fast-
| tokenizers-with... It's actually about the GraphQL parser
| mentioned in the original article.
| grncdr wrote:
| It's funny you mention libpq, because my first thought upon
| reading the article was "I wonder if a pure Ruby implementation
| of the postgres wire protocol could possibly lead to
| performance improvements?
| whizzter wrote:
| Iirc NodeJS started with a libpq binding but it was moved to
| pure-JS since as in the article, overhead of going in and out
| of JIT-land penalized it (as well as lacking any object
| optimizations).
| danmur wrote:
| This pure Python library claims quite fabulous performance:
| https://github.com/MagicStack/asyncpg
|
| I believe it because that team have done lots of great stuff
| but I haven't used it, I just remembered thinking it was
| interesting the performance was so good. Not sure how related
| it is to running on the asyncio loop (or which loop they used
| for benchmarks).
| coldtea wrote:
| > _Of course, there is massive overhead in doing so. Just look
| at go, which had to rewrite practically everything already
| available in go, and must always require a native
| implementation (protobuf for example shares the underlying
| interface across ruby, python, php... but then has a full
| separate implementation in go, and java I think)_
|
| Both Java and Go could just as well use FFI for those things
| "already available" as C.
|
| They don't because native (to the language) is better, and can
| be even faster (due to GC or lack of crossing the boundary).
|
| Python, PHP and co just take the easy way out. Plus, given
| their speed, if stuff like JSON and co were native-to-the-
| language they would be much slower than C extensions.
| iamgopal wrote:
| can numba and typed python will have similar effect ?
| KeplerBoy wrote:
| sure, in cases where Numba works well, it certainly is faster
| than a badly written C extension.
|
| It can however not be faster than a well written C extension.
| They're all just LLVM frontends.
| asah wrote:
| "faster than C" - TBH, I kinda dread these clickbait posts
| because faster than C doesn't matter in 2023, where speed is
| dictated by multi-core and GPU.
|
| Python is abysmally slow, but provides access to GPU and (soon)
| multi-core.
| [deleted]
| mgaunard wrote:
| Without control, power is nothing.
|
| C gives you control.
| ahoka wrote:
| It doesn't though.
| dvhh wrote:
| Well it gives you the belief that you are in control,
| because undefined behavior are implementation dependent,
| and that "modern processor" not only predict, but also can
| decide of the execution order of your nice code.
|
| That said, save for the Basic language, C is probably the
| lowest common denominator for system programming and for
| all the programming languages to interface with each other
| (with relative efficiency).
| ahoka wrote:
| Every time you would have direct control over the
| hardware it's considered UB and the compiler throws the
| code away.
| maccard wrote:
| Did you see the post yesterday about the people who deployed a
| kubernetes cluster because it took 24 hours to parse a few GB
| of CSVs in JavaScript?
| nottheengineer wrote:
| I'd like to read that but can't seem to find it. Do you
| remember the title or have a link?
| maccard wrote:
| Yep! I wish I was joking, but
| https://news.ycombinator.com/item?id=37415812
| dailykoder wrote:
| uhm, isn't the CPU access in python written in C (mostly)?
| astrobe_ wrote:
| One should remember that not all problems can be solve faster
| with concurrency - not all problems are "embarrassingly
| parallel". Besides, even when they are, one gets diminishing
| returns by adding more cores. Raw performance on single core is
| still relevant.
|
| That said, support for concurrency is relevant but more for
| "masking" I/O latency than for MIPS, a problem which is
| apparently more common. For a language created after 2010, it
| is nearly a fault not to have something less primitive than
| threads to handle concurrency. It is like still manually
| managing memory manually like C also does.
|
| I agree that "faster than C" claims too often hides some kind
| of cheating. But we have HN "reviewers" for that ;-).
| kazinator wrote:
| Ruby outperforms C _that is a Ruby extension, solving a problem
| by wrangling with Ruby types_.
|
| When you write a run-time for a dynamic, high level language, and
| then library functions in C which use the API's and objects of
| that run time, the resulting code is reasonably fast, but never
| as fast as solving the problem using direct C idioms.
|
| There are ways to make improvements in that type of code, but
| they tend to be tedious to code. For instance, you can use split
| the code into cases by type, and then use faster, more type-
| specific routines.
|
| Sometimes it's possible to get all the inputs out of the objects,
| work with lower, less encapsulated level C representations to do
| the bulk of the problem, and then put results back into the run
| time's objects again. Or do that partially at certain steps.
|
| In the TXR Lisp mapcar implementation, alloca is used to create a
| native C array of stack-allocated iterator objects. (For the
| parallel iteration over multiple sequences: mapcar is an N-ary
| function!). The element values are pulled from all these
| iterators and stuffed into a stack allocated args structure
| destructively; that same structure is re-used for each call to
| the projection function.
|
| That's not as fast as it could be. Beyond that it would be
| possible to separately handle the case when there is only one
| sequence argument, and to split that by type, and other such
| approaches.
|
| It's faster, though, than if it allocated a dynamic vector of
| dynamic iterators, and/or if it built a dynamic list of arguments
| in each iteration which were applied to the function as a list.
| john_the_writer wrote:
| I don't understand why we have to pretend Ruby could ever be as
| fast as C. Or faster. That's like saying a jockey is faster than
| their horse. Ruby is riding on C, so it cannot be faster.
|
| Also why even bother. Ruby is fast enough for the job it does.
| It's higher level and makes things that are "harder" in C,
| easier. Dev time is less, and thats where we win. We take the
| win, and accept what it is.
| [deleted]
| michaelteter wrote:
| I think the point of the essay is that we shouldn't immediately
| assume that going to C is the best way to speed up part of a
| Ruby system.
|
| It might be conventional wisdom to break out to C for the speed
| gain, but a purpose made, hand written piece of Ruby, with JIT
| optimizations, and be a better/faster solution.
| vidarh wrote:
| _Some_ Ruby implementations are "riding on C". Others are not,
| e.g. Truffle Ruby.
|
| While beating C overall would be _very hard_ because of the
| dynamism of Ruby, most Ruby code is not very dynamic after a
| short warmup (loading for most code; connecting a database etc.
| for code using a dynamic ORM), and at least on specific code it
| 's at least theoretically possible for a Ruby JIT to beat C by
| optimising to the actual runtime configuration and evaluate out
| all the dynamism that costs Ruby performance.
|
| TruffleRuby goes part-way there, in terms of guarded section
| that makes assumptions about types but where the guards shunts
| code that breaks those assumptions onto a slow path, but a Ruby
| JIT or a combination of a bytecode compiler and JIT, can at
| least in theory go _much_ further. E.g. a whole lot of the Ruby
| code library contains type checks that a compiler could hoist
| and /or elide.
|
| Yjit is a baby step, but a good start.
| VeejayRampay wrote:
| Hacker News is going to nitpick at the bold claim of the title
| instead of focusing on the fact that it's a well written
| technical article that underlines some neat tricks that allow a
| rather slow, highly dynamic language such as ruby to provide
| great performance under some circumstances, which is all that
| engineers should care about
| xxs wrote:
| Talking about technically well written:
|
| When showing microbenchmarks - show the exact hardware used,
| make sure the CPU does run at fixed frequency (no boost, most
| laptops can't control the freq. esp Macs)
| starbugs wrote:
| Critizising the title is not exactly nitpicking. The title is
| important. And it's misleading.
|
| I disagree that this article is well written. It is hard to
| identify what is compared exactly, at least as someone not
| deeply familiar with the ruby ecosystem. The article doesn't do
| a great job of explaining the caveats and limitations of the
| comparison either.
| coldtea wrote:
| > _Critizising the title is not exactly nitpicking. The title
| is important._
|
| Only if one doesn't bother to read the article. Else the
| title is just an insignificant detail, or, at worse, a
| redundant piece of promotional material attached to the
| article.
| starbugs wrote:
| And how do you decide if you're going to read the article?
| Ah yes, the title.
|
| I guess we will always find someone who tries to word-
| wrangle their way out of an obvious fact. The title is the
| first thing you see. It's printed in a bigger font than the
| rest. Most people won't even read anything else. In short:
| It's important. It's _not_ a detail. Hence it 's not
| nitpicking when I criticize it.
| coldtea wrote:
| > _And how do you decide if you 're going to read the
| article? Ah yes, the title._
|
| Nope: the subject. This is about Ruby and execution
| speed, for example.
|
| Then you open it and give it a quick cursory glance. Most
| articles have non-descriptive titles to begin with, or
| clickbait titles. The title could say any kind of non
| descriptive BS like "Ruby's new JIT rocks!" or "Ruby
| smoking C in benchmarks". That's even the case for
| "serious" media, like NYT.
|
| If you go by the title you're in for a bad experience.
| starbugs wrote:
| Exactly. This is why without the original title nobody
| who's not into Ruby would even consider reading it. And
| only a fraction of those people who are into Ruby and
| upvoted it would consider upvoting it. Thanks for
| confirming it.
| coldtea wrote:
| Well, it's not meant for people not into Ruby.
| starbugs wrote:
| Well, maybe read the original title again: "Ruby
| Outperforms C: Breaking the Catch-22"
| coldtea wrote:
| Still not meant for people not interested into Ruby.
| starbugs wrote:
| Got it. The title of an article is a detail and this
| title in particular doesn't draw any attention from
| people interested in C or the claims made by performance
| comparisons between different types of programming
| languages in general.
|
| That's probably also the reason why the title was changed
| on HN after this criticism was made and the submission
| disappeared from the front page a short while after that.
| bakugo wrote:
| If the writer of the article wanted people to actually read the
| article and judge it by its overall quality, they wouldn't have
| used such a blatantly clickbait-y title.
| 59nadir wrote:
| I agree; the title is doing a lot of work in terms of getting
| people to read the article and I'm sure the author is aware.
| This is a trade-off and when the article doesn't quite sell
| on the promise of the title you are bound to get a little
| backlash.
| thepostman0 wrote:
| Clickbait titles gets criticism because some people won't read
| beyond the title
| starbugs wrote:
| Clickbait title gets criticism, because it makes bold claims
| drawing the attention of people who when reading the article
| find that their time was unfairly wasted, because said claims
| are a pile of ...
|
| Excuse my French.
| lelanthran wrote:
| > Hacker News is going to nitpick at the bold claim of the
| title instead of focusing on the fact that it's a well written
| technical article t
|
| I think that's an unfair characterisation of HN readers.
|
| They aren't criticising the article after all, they're
| criticising a clickbait and misleading title.
| rewmie wrote:
| > Hacker News is going to nitpick at the bold claim of the
| title instead of focusing on the fact that (...)
|
| ...that the claims are somewhere between false and cherry-
| picked?
| wodenokoto wrote:
| If we are keeping the nitpickings to this thread, I think
| adding a "when" to the beginning would keep it just as
| interesting a title and be good enough in terms of correctness
| dig1 wrote:
| The title is misleading, just like other commenters mentioned.
| Just check how much indirection "rb_iv_get()" has to make (at
| the end, it will call [1], which isn't "a light" call). Now,
| check generated JIT code (in a blog post) for the same action
| where JIT knows how to shave off unnecessary indirection.
|
| We are comparing apples and oranges here.
|
| [1]
| https://github.com/ruby/ruby/blob/b635a66e957e4dd3fed83ef1d7...
| vidarh wrote:
| Yes, it may well be misleading, yet you're making the point
| of the commenter you replied to _while pointing out_ why the
| article is interesting. If you 're a position where you're
| using Ruby and considering whether writing a C extension will
| speed things up, it has valuable advice on what other things
| to also try. That is a lot more interesting to me than the
| title.
| xxs wrote:
| The article findings apply to any managed language. Don't
| call extern libs often as the call overhead is non-trivial
| and it cannot be inlined. There's nothing new about
| vidarh wrote:
| _That one part_ is nothing new. That does not mean the
| specific mechanisms of what to do to take advantage of
| this for Ruby isn 't of interest to Ruby developers, who
| are the main audience for this given it's posted on the
| blog for Shopify's Ruby/Rails team.
| anonymoushn wrote:
| The fastest parser in the post works at 35MB/s which is ~100x
| slower than necessary, improved from a baseline of ~1000x
| slower than necessary.
| [deleted]
___________________________________________________________________
(page generated 2023-09-09 23:02 UTC)