Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make float16 handling more performant #1379

Merged
merged 16 commits into from
Nov 15, 2024
Merged

Conversation

LeviPesin
Copy link
Contributor

Hello!

Sorry I'm not really familiar with the code style of core-js -- feel free to modify this PR in any way.

This PR adds methods for packing/unpacking float16 values (instead of those from internals/ieee754.js) based on my code from here (roundTiesToEven function taken from internals/math-float-round.js). They should be 100% IEEE754-compliant but are untested (would be great if you could help with that).

Packing is appoximately 15-20 times faster on my machine (running on an array of thousand values, ~16K ops/s vs ~900 ops/s) tested with the following JSBench code:

Setup
const N = 1E3;

const floats = [];
const results = [];

for (let i = 0; i < N; i++) {
  floats[i] = Math.random() * 2 ** (10 * Math.floor(Math.random()) - 20);
  results[i] = 0;
}

const minInfinity16 = (2 - 2 ** -11) * 2 ** 15, minNormal16 = (1 - 2 ** -11) * 2 ** -14, recMinSubnormal16 = 2 ** 10 * 2 ** 14, recSignificandDenom16 = 2 ** 10;

var EPSILON = 2.220446049250313e-16; // Number.EPSILON
var INVERSE_EPSILON = 1 / EPSILON;

var roundTiesToEven = function (n) {
return n + INVERSE_EPSILON - INVERSE_EPSILON;
};

function toFloat16(value) {
  if (Number.isNaN(value)) return 0x7e00; // NaN
  if (value === 0) return (1 / value === -Infinity) << 15; // +0 or -0
  const neg = value < 0;
  if (neg) value = -value;
  if (value >= minInfinity16) return neg << 15 | 0x7c00; // Infinity
  if (value < minNormal16) return neg << 15 | roundTiesToEven(value * recMinSubnormal16); // subnormal
  // normal
  const exponent = Math.log2(value) | 0;
  if (exponent === -15) return neg << 15 | recSignificandDenom16; // we round from a value between 2 ** -15 * (1 + 1022/1024) (the largest subnormal) and 2 ** -14 * (1 + 0/1024) (the smallest normal) to the latter (former impossible because of the subnormal check above)
  const significand = roundTiesToEven((value * 2 ** -exponent - 1) * recSignificandDenom16);
  if (significand === recSignificandDenom16) return neg << 15 | exponent + 16 << 10; // we round from a value between 2 ** n * (1 + 1023/1024) and 2 ** (n + 1) * (1 + 0/1024) to the latter
  return neg << 15 | exponent + 15 << 10 | significand;
}

var $Array = Array;
var abs = Math.abs;
var pow = Math.pow;
var floor = Math.floor;
var log = Math.log;
var LN2 = Math.LN2;

var pack = function (number, mantissaLength, bytes) {
var buffer = $Array(bytes);
var exponentLength = bytes * 8 - mantissaLength - 1;
var eMax = (1 << exponentLength) - 1;
var eBias = eMax >> 1;
var rt = mantissaLength === 23 ? pow(2, -24) - pow(2, -77) : 0;
var sign = number < 0 || number === 0 && 1 / number < 0 ? 1 : 0;
var index = 0;
var exponent, mantissa, c;
number = abs(number);
// eslint-disable-next-line no-self-compare -- NaN check
if (number !== number || number === Infinity) {
  // eslint-disable-next-line no-self-compare -- NaN check
  mantissa = number !== number ? 1 : 0;
  exponent = eMax;
} else {
  exponent = floor(log(number) / LN2);
  c = pow(2, -exponent);
  if (number * c < 1) {
    exponent--;
    c *= 2;
  }
  if (exponent + eBias >= 1) {
    number += rt / c;
  } else {
    number += rt * pow(2, 1 - eBias);
  }
  if (number * c >= 2) {
    exponent++;
    c /= 2;
  }
  if (exponent + eBias >= eMax) {
    mantissa = 0;
    exponent = eMax;
  } else if (exponent + eBias >= 1) {
    mantissa = (number * c - 1) * pow(2, mantissaLength);
    exponent += eBias;
  } else {
    mantissa = number * pow(2, eBias - 1) * pow(2, mantissaLength);
    exponent = 0;
  }
}
while (mantissaLength >= 8) {
  buffer[index++] = mantissa & 255;
  mantissa /= 256;
  mantissaLength -= 8;
}
exponent = exponent << mantissaLength | mantissa;
exponentLength += mantissaLength;
while (exponentLength > 0) {
  buffer[index++] = exponent & 255;
  exponent /= 256;
  exponentLength -= 8;
}
buffer[index - 1] |= sign * 128;
return buffer;
};
Original implementation case
for (let i = 0; i < N; i++) results[i] = pack(floats[i], 10, 2);
New implementation case
for (let i = 0; i < N; i++) results[i] = toFloat16(floats[i]);

@zloirock
Copy link
Owner

Local tests running could significantly save your time.

@LeviPesin
Copy link
Contributor Author

LeviPesin commented Nov 11, 2024

21:7 error ES2015 'Number.isNaN' method is forbidden es/no-number-isnan
28:18 error ES2015 'Math.log2' method is forbidden es/no-math-log2

Should I use value !== value and Math.log(value) / Math.LN2?

21:35 error Invalid number literal casing unicorn/number-literal-case
25:50 error Invalid number literal casing unicorn/number-literal-case

Not sure what these errors mean, do they happen because of implicit conversion of boolean neg to number? No, they happen because of using 0xsomething syntax, should I remove it?

@zloirock
Copy link
Owner

zloirock commented Nov 11, 2024

Should I use value !== value

Yes. With disabling eslint warnings like

  // eslint-disable-next-line no-self-compare -- NaN check
  if (value !== value) return value;

Math.log(value) / Math.LN2?

Yes. If it will be required, I'll update it later.

unicorn/number-literal-case

https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/number-literal-case.md

@zloirock
Copy link
Owner

Stylistically LGTM, current tests passed.

I'll think about the logic and whether it's worth adding any more tests.

Thanks!

@zloirock zloirock merged commit a1298dc into zloirock:master Nov 15, 2024
23 checks passed
@LeviPesin LeviPesin deleted the patch-1 branch November 16, 2024 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants