dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.18k stars 1.56k forks source link

int.reinterpetAsDouble() and friends #46195

Open Hixie opened 3 years ago

Hixie commented 3 years ago

Right now, to cast an int to a double (as is commonly needed when parsing or generating binary formats), you have to create a ByteData, push the bytes into it, then read the data out of it, as in:

    final ByteData _buffer = ByteData(4);
    _buffer.setUint32(0, (byte1 & 0xFC) + (byte2 << 8) + (byte3 << 16) + (byte4 << 24));
    return _buffer.getFloat32(0);

I'm just guessing but it seems like this is not as efficient as it could be. It would be really nice if there was a way to do a zero-overhead cast from int to 32bit double, int to 64bit double, 64bit double to int, and double-to-32bit-double-to-int (that last one wouldn't be quite zero-overhead presumably).

    return ((byte1 & 0xFC) + (byte2 << 8) + (byte3 << 16) + (byte4 << 24))).reinterpretAsFloat32();
nigeltao commented 3 years ago

FWIW, here's how it works in Go. https://golang.org/pkg/math/#Float32frombits declares:

func Float32frombits(b uint32) float32

From the user's point of view, this is like any other top-level function. The compilers can recognize calls to it, though, and replace it with a no-op (at the CPU instruction level).

The function name itself is a little awkward, but we're stuck with it now.

lrhn commented 3 years ago

First of all, I'd expect to read that kind of binary data directly from a ByteData object, not to read the bytes first, then convert them to something else afterwards. I can see the problem if the bytes of a float are not consecutive in the physical layout, but that sounds rare. (Or, if the format is compressed or otherwise delivered in chunks).

This sounds like something which belongs in typed_data, and something which won't work (not to its full extent) when compiled to JavaScript. There are no 64-bit integers in JavaScript, and all integers are already doubles.

Even in JavaScript, an int32asFloat32 can work, and float32asInt32 too. (Should the Int be signed or unsigned? Probably signed, it makes the result have the same sign as the original, and JS bit operations prefers signed).

It's only going to be zero overhead if the numbers are unboxed. Dart currently boxes doubles on the heap, and integers above the Smi range too (which is 63 bits on 64-bit architectures), and even smis are tagged. The basic int64asFloat64 operation would smi-check (assume it succeeds), then untag, then allocate a heap-double and store the untagged value into that. Obviously, if the numbers are unboxed. a lot of that can go away, but you can't keep numbers unboxed for long.

Could this be done sufficiently efficiently using dart:ffi?

Alternatively, I'd just make a helper library using a ByteBuffer:

final _buffer = ByteBuffer(8);
final _int32 = _buffer.asInt32List();
final _float32 = _buffer.asFloat32List();
final _int64 = _buffer.asInt64List();
final _float64 = _buffer.asFloat64List();
int float32ToInt32(double f) {
  _float32[0] = f;
  return _int32[0];
}
double int32ToFloat32(int i) {
  _int32[0] = f;
  return _float[0];
}
// ... where the _int64 won't work on the web
Hixie commented 3 years ago

FWIW https://github.com/google/iconvg/blob/main/src/dart/lib/decoder.dart is the code that led to this issue being filed. I welcome suggestions for how to restructure that code to make it faster (for my own edification mainly, I don't expect that code to see much usage in practice). (There's a couple of places where we're aware it could be made much faster, mostly around inlining the calls to nextByte where that could avoid the range checks, and some similar fixes elsewhere; I've intentionally optimized for code maintenance over speed where there would be a trade-off because the spec is likely to change still. But it's possible that the use of ByteData/Uint8List etc is suboptimal in ways that are orthogonal to maintenance costs.)

droplet-js commented 2 years ago

any news update?

lrhn commented 2 years ago

No update.

The recommended approach is to use a ByteData view on your bytes and read through that, or more specific types if the data is known to be aligned.

There is no possible cross-platform way to convert a single 64-bit int to a double with the same representation, because the web does not have 64-bit ints at all. The only way to have a 64-bit int is to store it in a typed-data array (or another special representation like the fixnum package's Int64/Uint64 classes).

It would be possible to be cross platform for "float" (32-bit floating point numbers), but as far as I know, the web compiler would have to use a typed-data array for the conversion anyway.

So I'd use something like:

final ByteData _buffer = ByteData(8);
int doubleBits(double value) => (_buffer..setFloat64(0, value)).getInt64(0);
double reinterpretAsDouble(int value) => (_buffer..setInt64(0, value)).getFloat64(0);
double reinterpretAsFloat(int value) => (_buffer..setInt32(0, value)).getFloat32(0);
/// etc.
Hixie commented 2 years ago

The approach we use in Flutter is to provide the best we can on each platform, rather than try to be limited to the least common denominator. It's ok if something can be a zero-op cast on x64 but ends up being more expensive in dart compiled to JS. The point is to do the best we can on each platform, and the best on x64 is to literally take the 8 bytes and just treat them as a double instead of an int.