2

For some reason I was thinking since TypedArrays represent sequences of binary signed and unsigned integers, we could use normal operators to add, subtract, multiply, and divide them.

With bitwise operators it doesn't work:

const a = new Uint8Array([0x1, 0x1, 0x1, 0x1])
// 4369, 1000100010001b

const b = new Uint8Array([0x1, 0x1, 0x1, 0x1])
// 4369, 1000100010001b

const c = a >>> 8 // desired outcome: 10001
const d = a & b   // desired outcome: 1000100010001
const e = a | b   // desired outcome: 1000100010001
const f = a ^ b   // desired outcome: 0111011101110

// actual results:

console.log('c:', c) // 0
console.log('d:', d) // 0
console.log('e:', e) // 0
console.log('f:', f) // 0

And neither does it work with arithmetic operators:

const a = new Uint8Array([0x1, 0x1, 0x1, 0x1]) // 4369
const b = new Uint8Array([0x1, 0x1, 0x1, 0x1]) // 4369
const c = a + b 
const d = a - b
const e = a * b
const f = a / b

// desired:
console.log(c === new Uint8Array([0x2, 0x2, 0x2, 0x2])) // want: true
console.log(d === new Uint8Array([0x0, 0x0, 0x0, 0x0])) // want: true
console.log(e === new Uint8Array([0x1, 0x1, 0x1, 0x1])) // want: true
console.log(f === new Uint8Array([0x1, 0x1, 0x1, 0x1])) // want: true

// but those are all false, instead:
console.log('c:', c) // 1,1,1,11,1,1,1
console.log('d:', d) // NaN
console.log('e:', e) // NaN
console.log('f:', f) // NaN

I tried using the ternary operators to fix this problem, but that didn't work either. I'm sure I could do some pretty inefficient conversions from Typed Array to number and back, but I imagine there's a better way. I've googled, and browsed the docs and nothing's popping out at me. What's the most efficient way to actually perform bitwise and arithmetic operations between Typed Arrays?

J.Todd
  • 707
  • 1
  • 12
  • 34
  • 2
    "*I could do some pretty inefficient conversions from Typed Array to number and back*" - what makes you think these are inefficient? And no, you *must* convert them to numbers, arithmetic operations are only available on numbers. – Bergi May 23 '21 at 16:39
  • 2
    I'm not sure why you'd think that--they're arrays, not numbers. Saying that `[1, 1, 1, 1]` "is" `4369` isn't correct at all from JS's point of view, only yours, because you happen to know what that sequence "means" (and its endian-ness etc). If you want to operate on them as numbers you have to turn them into numbers. Bitwise ops on arrays of equal sizes are trivial--just handle each element in turn. Arithmetic ops require arithmetic types. There might be some bit-manipulation libs that do this; not sure--I just rolled my own. – Dave Newton May 23 '21 at 16:39
  • 1
    I was assuming you already had a solution to this [in yesterday's question](https://stackoverflow.com/q/67651711/1048572). Doesn't your ALU work? – Bergi May 23 '21 at 16:42
  • @Bergi Well I didnt think I actually had to code a proper ALU. That was the problem I was starting to work on. I thought JavaScript operators were just going to work. I mean it is a sequence of bytes, why can't it work natively? I think some of the hardware operations that happen with an ALU are going to be pretty inefficient to replicate in JS, so I was hoping to find a feature in the faster mechanisms written into V8 that would make it more performant. – J.Todd May 23 '21 at 17:00
  • @Bergi Maybe web assembly allows this? Havent learned wasm yet. And I say *"inefficient"* because at the core level, we can multiply the locations in memory these Typed Arrays represent with a few basic Assembly instructions, so why would JS not be able to implement it? Inefficient in the sense that any conversion at all seems unnecessary fundamentally. – J.Todd May 23 '21 at 17:03
  • @J.Todd Actually yes, `registers.rax.write(registers.rax.read() + registers.rbx.read())` or `registers.ebx.write(registers.ecx.read() & registers.eax.read())` would just work. One makes JS code performant by writing idiomatic and straightforward code for which the V8 optimising compiler will emit fast assembly. Not by trying to embed native assembler instructions into JS code. If you want to do that, yes, go for WebAssembly. – Bergi May 23 '21 at 17:07
  • @Bergi Ah yes because in the code you wrote you're accessing an index on the BigUint64Array, which is just a BigInt. I was hoping to follow your suggestion of not using BigInt for everything, but I think in this case, in JS it might be more efficient that the alternatives for accessing parts of an ArrayBuffer as operands. – J.Todd May 23 '21 at 17:13
  • @Bergi I'll have to do some benchmarks, but perhaps adding together the number values of sub-64-bit views would be more efficient than defaulting to BigInts. But perhaps the lower number of iterations looping over values / adding them to sub-64-bit operands (instead combining one or perhaps two indexes of a `BigUint64Array`) will make the BigInt the winner. Do you have a guess as to the answer? – J.Todd May 23 '21 at 17:24
  • 1
    It might be more efficient to use normal numbers where possible (depends on how well bigints are optimised yet by the engine), but this requires additional housekeeping to avoid mixing bigints with non-bigints, and polymorphic ALU code. (Or is it an implicit property of x86 that one cannot mix operands of different bit sizes? I don't know, so I was going the conservative route of treating everything as a bigint for simplicity). Get it working first, then optimise later (with benchmarks :D)! – Bergi May 23 '21 at 17:26

1 Answers1

1

The answer, with credit to @Bergi from his other answer, was already apparent, although I didn't realize it. Multiples of 64 bits of an ArrayBuffer can be accessed as a single BigInt by utilizing the BigUint64Array view, and then math can be done on the arrays by accessing one or more indexes (combining them, in the latter case), and used as a normal operand with any of the desired arithmetic / bitwise operators.

J.Todd
  • 707
  • 1
  • 12
  • 34