On Tue, Feb 02, 2021 at 08:50:56PM -0700, ben via cctalk wrote:
On 2/1/2021 6:07 AM, Peter Corlett via cctalk wrote:
[...]
> You're describing a failing in C and similar
languages stuck in the
> 1960s. Here's a Rust method that does add-exposing-carry:
So
why could this not have been done 30 earlier?
(I assume you mean "30 years earlier" here.)
Two reasons, one of which I was already aware of back then in 1991, and one
which is only obvious in hindsight.
The first is simple toxic masculinity. It's more manly to write stuff in
machine code (then) or C (now), apparently.
The second is a combination of Moore's Law and computer science. To handwave
wildly, Rust is what you get when you look at C++'s mistakes of the last
forty years and start again. If you've used a C++ compiler from the 1980s or
early 1990s, you'll have found the experience harrowing: computers were
still too slow and too small to do a good job, and various important
language design and code-generation techniques were not yet known and/or
were too impractical to implement. It all improved rapidly in the 1990s and
2000s, bringing us more or less to the top of the sigmoid curve.
u32::overflowing_add() returns a (u32, bool), i.e. a two-member struct.
Those early compilers cannot return structs directly, so the caller would
have to reserve space (on the stack, probably) and pass the address for the
function to fill in. That simple add-producing-carry has just become a
function with a half-dozen instructions, several of which are needed to
check the carry flag and convert it into a integer, plus many memory
accesses, just to provide a C wrapper around a simple add instruction. At
that point it does make sense to toss the compiler and write assembly
language.
Modern compilers do a lot of heavy inlining as a matter of course, will
split structs and perform dataflow analysis on individual elements, and
generally avoid memory access unless they absolutely have to.
u32::overflowing_add() should turn into a single add instruction and the
carry flag will probably be tested directly by a branch instruction and
never get converted to a boolean variable. It'll be as good as if not better
than hand-written assembler.
This is a particularly trivial function as well. Rust just wouldn't be
viable with 30 year old compiler technology.
The
documentation doesn't explicitly say "carry" because Rust is
architecture-neutral and it's down to LLVM to decide how to express it in
machine code, but on x86 (and probably ARM) the boolean return value
comes directly from the carry flag.
mincing words, sigh.
RISC-V doesn't have a carry flag. It handles overflow by e.g. using the BLT
instruction to branch if the result is smaller than the number being added
to. So documentation which assumes the world is x86 will not make sense here.
[...]
> You "don't believe in objects" yet
then describe a problem which only
> exists due to the lack of them and then present OO pseudocode to solve
> it. A lot of OO languages suck of course, but the fundamental idea of
> encapsulation is not the bit that sucks.
Are objects? they only way to solve this problem. I
see a object as data
structure tied to some code. What I would like to see data structures
having fixed constants like array start and end of a structure for a array
as variables when called into use.
There's no real difference between a "constant ... when called into use"
and
a pure function which returns a value based on the structure elements.
> Here's it in Rust, where it takes in an
arbitrary
> array (pedantically, "slice", a (pointer, element count)-tuple) and
> determines its length at runtime:
>
> pub fn clear_indexed(array: &mut [usize]) {
> for index in 0 .. array.len() {
> array[index] = 0;
> }
> }
I don't want Information hiding, I want to know
what it does clearly. If I
can't figure it out how can a computer program do it. Where does ".len()"
find the size?
That is an implementation detail which you don't need to know to be able to
write Rust code. However, the len() method actually just returns the element
count from the (pointer, element count)-tuple. The function is so trivial
that it is guaranteed to be inlined so it's exactly the same as if you had
accessed the field directly.
Before you ask, no, you can't access the field directly for all of the
excellent reasons explained by every "introduction to OOP" tutorial which I
don't need to repeat.
[...]
> pub fn clear_iterator(array: &mut [usize]) {
> for elem in array {
> *elem = 0;
> }
> }
Both code
fragments generate equivalent assembly in this trivial example
because the Rust compiler could prove at compile time that the index
variable can't go out-of-bounds. In more complex real-world code it
cannot reliably do so and will insert a run-time check which aborts if
the index is out-of-bounds. Or if it's C, trundle on and corrupt things.
Why
prove it? Just test it at runtime.
Runtime bounds-checking can get rather expensive -- it tends to add an extra
compare-and-branch to tight inner loops and may also foil automatic
vectorisation -- so it makes code much faster if the compiler can prove the
check is unnecessary and eliminate it, or at least hoist it out of loops.
An ordinary Rust programmer doesn't actually need to know any of this. They
write code and it goes as fast as is safe. If it's not fast enough for them,
they can ask an expert who knows this sort of intimate low-level detail and
optimisation tricks to poke at their code, much as with any other language.
What decades of toxic-masculinity C programmers have taught us is that
they'll eliminate the bounds-check without proving it was unnecessary,
assuming they ever bothered to perform the test in the first place, and ship
code which will be trivially compromised. But it'll be really fast right up
until your credit card details are leaked!