[core] Add trailingZeroBitCount and oneBitCount to int
Adds two new getters to int for bit-counting: trailingZeroBitCount (ctz) and oneBitCount (popcount). On native platforms they operate on the full 64-bit two's-complement representation; on the web they operate on the least-significant 32 bits. Implementations: - VM: unified C++ natives Integer_trailingZeroBitCount / Integer_oneBitCount on _IntegerImplementation, using Utils::CountTrailingZeros64 and Utils::CountOneBits64. The receiver may be _Smi or _Mint at runtime. - dart2js / DDC: clz32-based ctz and a SWAR popcount. - dart2wasm: inlined i64.ctz and i64.popcnt intrinsics. leadingZeroBitCount (clz) is intentionally excluded from this CL: its result depends on the platform integer width (e.g. 1.leadingZeroBitCount is 31 on web, 63 on native), and the same value can be derived from the existing bitLength getter when needed. Asm intrinsification on native architectures is intentionally left for a separate follow-up CL. Work towards https://github.com/dart-lang/sdk/issues/6486 (this CL covers popcount and ctz from the bit-twiddling list; clz, rotate, reverse, and others remain). Work towards https://github.com/dart-lang/sdk/issues/1053 (efficient BitSet implementation). Bug: https://github.com/dart-lang/sdk/issues/52673 Bug: https://github.com/dart-lang/sdk/issues/38346 TEST=tests/corelib/int_bit_count_test Change-Id: I8a5cdb5c91360478f47bbd6b9c84ca1c477aa8c7 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/498041 Reviewed-by: Slava Egorov <vegorov@google.com> Commit-Queue: Slava Egorov <vegorov@google.com> Reviewed-by: Stephen Adams <sra@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com> Auto-Submit: Modestas Valauskas <valauskasmodestas@gmail.com> Reviewed-by: Lasse Nielsen <lrn@google.com>
This commit is contained in:
committed by
dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
756dd1c665
commit
754239b077
@@ -74,6 +74,14 @@ To learn more about the feature, check out the
|
||||
- Added `List.unmodifiableOf` with better typing than `List.unmodifiable`.
|
||||
- Added `Map.unmodifiableOf` with better typing than `Map.unmodifiable`.
|
||||
|
||||
#### `dart:core`
|
||||
|
||||
- Added two getters on `int` for efficient bit-counting:
|
||||
`trailingZeroBitCount` (ctz) and `oneBitCount` (popcount). On native
|
||||
platforms they operate on the full 64-bit two's-complement
|
||||
representation; on the web they operate on the least-significant 32
|
||||
bits. See [#52673](https://github.com/dart-lang/sdk/issues/52673).
|
||||
|
||||
#### `dart:io`
|
||||
|
||||
- The cookie-date parser now uses the correct algorithm again.
|
||||
|
||||
@@ -629,6 +629,21 @@ class Intrinsifier {
|
||||
return w.NumType.i64;
|
||||
}
|
||||
|
||||
// int.trailingZeroBitCount
|
||||
if (cls == translator.coreTypes.intClass &&
|
||||
name == 'trailingZeroBitCount') {
|
||||
codeGen.translateExpression(receiver, w.NumType.i64);
|
||||
b.i64_ctz();
|
||||
return w.NumType.i64;
|
||||
}
|
||||
|
||||
// int.oneBitCount
|
||||
if (cls == translator.coreTypes.intClass && name == 'oneBitCount') {
|
||||
codeGen.translateExpression(receiver, w.NumType.i64);
|
||||
b.i64_popcnt();
|
||||
return w.NumType.i64;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,26 @@ DEFINE_NATIVE_ENTRY(Smi_bitLength, 0, 1) {
|
||||
return Smi::New(result);
|
||||
}
|
||||
|
||||
// Unified bit-count natives. Receiver is _IntegerImplementation, so the
|
||||
// operand can be either Smi or Mint at runtime.
|
||||
DEFINE_NATIVE_ENTRY(Integer_trailingZeroBitCount, 0, 1) {
|
||||
const Integer& operand =
|
||||
Integer::CheckedHandle(zone, arguments->NativeArgAt(0));
|
||||
intptr_t result =
|
||||
Utils::CountTrailingZeros64(static_cast<uint64_t>(operand.Value()));
|
||||
ASSERT(Smi::IsValid(result));
|
||||
return Smi::New(result);
|
||||
}
|
||||
|
||||
DEFINE_NATIVE_ENTRY(Integer_oneBitCount, 0, 1) {
|
||||
const Integer& operand =
|
||||
Integer::CheckedHandle(zone, arguments->NativeArgAt(0));
|
||||
intptr_t result =
|
||||
Utils::CountOneBits64(static_cast<uint64_t>(operand.Value()));
|
||||
ASSERT(Smi::IsValid(result));
|
||||
return Smi::New(result);
|
||||
}
|
||||
|
||||
// Should be kept in sync with il_*.cc EmitHashIntegerCodeSequence
|
||||
uint32_t Multiply64Hash(int64_t ivalue) {
|
||||
const uint64_t magic_constant = /*0x1b873593cc9e*/ 0x2d51;
|
||||
|
||||
@@ -66,6 +66,8 @@ namespace dart {
|
||||
V(SendPort_sendInternal_, 2) \
|
||||
V(Smi_bitNegate, 1) \
|
||||
V(Smi_bitLength, 1) \
|
||||
V(Integer_trailingZeroBitCount, 1) \
|
||||
V(Integer_oneBitCount, 1) \
|
||||
V(SuspendState_instantiateClosureWithFutureTypeArgument, 2) \
|
||||
V(Mint_bitNegate, 1) \
|
||||
V(Mint_bitLength, 1) \
|
||||
|
||||
@@ -458,6 +458,26 @@ final class JSNumber extends Interceptor
|
||||
return JS('!', 'Math.clz32(#)', uint32);
|
||||
}
|
||||
|
||||
@notNull
|
||||
int get trailingZeroBitCount {
|
||||
int v = JS<int>('!', '# | 0', this);
|
||||
if (v == 0) return 32;
|
||||
return JS<int>('!', '31 - Math.clz32(# & -#)', v, v);
|
||||
}
|
||||
|
||||
@notNull
|
||||
int get oneBitCount {
|
||||
// The number of bits set in the least significant 32 bits of `this`,
|
||||
// also known as the "Hamming weight". See:
|
||||
// https://en.wikipedia.org/wiki/Hamming_weight for an explanation of the
|
||||
// following algorithm.
|
||||
int v = JS<int>('!', '# | 0', this);
|
||||
v -= (v >>> 1) & 0x5555_5555;
|
||||
v = (v & 0x3333_3333) + ((v >>> 2) & 0x3333_3333);
|
||||
v = (v + (v >>> 4)) & 0x0f0f_0f0f;
|
||||
return JS<int>('!', '(Math.imul(#, 0x01010101) >>> 24)', v);
|
||||
}
|
||||
|
||||
// Returns pow(this, e) % m.
|
||||
@notNull
|
||||
int modPow(@nullCheck int e, @nullCheck int m) {
|
||||
|
||||
@@ -547,6 +547,24 @@ final class JSInt extends JSNumber implements int, TrustedGetRuntimeType {
|
||||
return JS('JSUInt31', 'Math.clz32(#)', uint32);
|
||||
}
|
||||
|
||||
int get trailingZeroBitCount {
|
||||
int v = JS<int>('int', '# | 0', this);
|
||||
if (v == 0) return 32;
|
||||
return JS<int>('JSUInt31', '31 - Math.clz32(# & -#)', v, v);
|
||||
}
|
||||
|
||||
int get oneBitCount {
|
||||
// The number of bits set in the least significant 32 bits of `this`,
|
||||
// also known as the "Hamming weight". See:
|
||||
// https://en.wikipedia.org/wiki/Hamming_weight for an explanation of the
|
||||
// following algorithm.
|
||||
int v = JS<int>('int', '# | 0', this);
|
||||
v -= (v >>> 1) & 0x5555_5555;
|
||||
v = (v & 0x3333_3333) + ((v >>> 2) & 0x3333_3333);
|
||||
v = (v + (v >>> 4)) & 0x0f0f_0f0f;
|
||||
return JS<int>('JSUInt31', '(Math.imul(#, 0x01010101) >>> 24)', v);
|
||||
}
|
||||
|
||||
// Returns pow(this, e) % m.
|
||||
int modPow(int e, int m) {
|
||||
if (e is! int) {
|
||||
|
||||
@@ -162,6 +162,15 @@ abstract final class _IntegerImplementation implements int {
|
||||
@pragma("vm:exact-result-type", bool)
|
||||
@pragma("vm:external-name", "Integer_equalToInteger")
|
||||
external bool _equalToInteger(int other);
|
||||
|
||||
@pragma("vm:exact-result-type", "dart:core#_Smi")
|
||||
@pragma("vm:external-name", "Integer_trailingZeroBitCount")
|
||||
external int get trailingZeroBitCount;
|
||||
|
||||
@pragma("vm:exact-result-type", "dart:core#_Smi")
|
||||
@pragma("vm:external-name", "Integer_oneBitCount")
|
||||
external int get oneBitCount;
|
||||
|
||||
int abs() {
|
||||
return this < 0 ? -this : this;
|
||||
}
|
||||
|
||||
@@ -432,6 +432,8 @@ final class BoxedInt implements int {
|
||||
@pragma("wasm:intrinsic")
|
||||
external int operator ~();
|
||||
external int get bitLength;
|
||||
external int get trailingZeroBitCount;
|
||||
external int get oneBitCount;
|
||||
|
||||
@override
|
||||
external String toString();
|
||||
|
||||
@@ -256,6 +256,46 @@ abstract final class int extends num {
|
||||
/// ```
|
||||
int get bitLength;
|
||||
|
||||
/// The number of trailing (least significant) zero bits in the binary
|
||||
/// representation of this integer.
|
||||
///
|
||||
/// On JavaScript platforms, only the least significant 32 bits are used.
|
||||
/// On native platforms, the 64-bit signed integer is used directly.
|
||||
///
|
||||
/// The trailing zero-bit count is the position of the least significant
|
||||
/// 1-bit in the binary representation of the integer. If the integer is
|
||||
/// zero, the value is the size of integer that the platform uses for bit
|
||||
/// operations (64-bit on native, 32-bit on the web).
|
||||
/// ```dart
|
||||
/// 1.trailingZeroBitCount; // 0
|
||||
/// 2.trailingZeroBitCount; // 1
|
||||
/// 8.trailingZeroBitCount; // 3
|
||||
/// 0.trailingZeroBitCount; // 64 on native, 32 on the web
|
||||
/// ```
|
||||
@Since("3.13")
|
||||
int get trailingZeroBitCount;
|
||||
|
||||
/// The number of `1` bits in the binary representation of this integer.
|
||||
///
|
||||
/// On JavaScript platforms, only the least significant 32 bits are used.
|
||||
/// On native platforms, the 64-bit signed integer is used directly.
|
||||
///
|
||||
/// The one-bit count is the number of `1` digits in the binary
|
||||
/// representation of that integer. A negative integer has one-digits up to
|
||||
/// the size of integer that the platform uses for bit operations (64-bit
|
||||
/// on native, 32-bit on the web).
|
||||
/// The value of `n.oneBitCount + (~n).oneBitCount` is always the size the
|
||||
/// platform uses for bit operations (on the web, at least if the value
|
||||
/// starts out as a 32-bit integer).
|
||||
/// ```dart
|
||||
/// 0.oneBitCount; // 0
|
||||
/// 1.oneBitCount; // 1
|
||||
/// 7.oneBitCount; // 3
|
||||
/// (-1).oneBitCount; // 64 on native, 32 on the web
|
||||
/// ```
|
||||
@Since("3.13")
|
||||
int get oneBitCount;
|
||||
|
||||
/// Returns the least significant [width] bits of this integer as a
|
||||
/// non-negative number (i.e. unsigned representation). The returned value has
|
||||
/// zeros in all bit positions higher than [width].
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
//
|
||||
// Testing int.trailingZeroBitCount and int.oneBitCount.
|
||||
|
||||
import "package:expect/expect.dart";
|
||||
import "package:expect/variations.dart" show jsNumbers;
|
||||
|
||||
// Platform width for bit operations: 64 with native integer semantics
|
||||
// (VM, dart2wasm), 32 with JS-number semantics (dart2js, DDC).
|
||||
const int width = jsNumbers ? 32 : 64;
|
||||
|
||||
// `js == null` means the case is only meaningful on a native (64-bit)
|
||||
// implementation and the JS assertion is skipped.
|
||||
void checkTrailing(int i, int native, int? js) {
|
||||
final expected = jsNumbers ? js : native;
|
||||
if (expected == null) return;
|
||||
Expect.equals(expected, i.trailingZeroBitCount, '$i.trailingZeroBitCount');
|
||||
}
|
||||
|
||||
void checkOne(int i, int native, int? js) {
|
||||
final expected = jsNumbers ? js : native;
|
||||
if (expected == null) return;
|
||||
Expect.equals(expected, i.oneBitCount, '$i.oneBitCount');
|
||||
}
|
||||
|
||||
void testTrailingZeroBitCount() {
|
||||
// Zero: trailing count equals full platform width.
|
||||
checkTrailing(0, 64, 32);
|
||||
|
||||
// Positive values.
|
||||
checkTrailing(1, 0, 0);
|
||||
checkTrailing(2, 1, 1);
|
||||
checkTrailing(3, 0, 0);
|
||||
checkTrailing(4, 2, 2);
|
||||
checkTrailing(8, 3, 3);
|
||||
checkTrailing(0x10, 4, 4);
|
||||
checkTrailing(4096, 12, 12);
|
||||
checkTrailing(0x8000_0000, 31, 31);
|
||||
checkTrailing(0x7fff_ffff, 0, 0);
|
||||
|
||||
// Negative values: two's complement preserves low bits.
|
||||
checkTrailing(-1, 0, 0);
|
||||
checkTrailing(-2, 1, 1);
|
||||
checkTrailing(-4, 2, 2);
|
||||
checkTrailing(-1024, 10, 10);
|
||||
checkTrailing(-0x4000_0000, 30, 30);
|
||||
checkTrailing(-0x8000_0000, 31, 31);
|
||||
|
||||
// Values above the 32-bit range. On JS the operation is defined as
|
||||
// counting trailing zeros of the low 32 bits, so any value whose low
|
||||
// 32 bits are zero yields the JS platform width (32) regardless of
|
||||
// what's above.
|
||||
checkTrailing(0x1_0000_0000, 32, 32);
|
||||
checkTrailing(0x2_0000_0000, 33, 32);
|
||||
checkTrailing(0x4_0000_0000, 34, 32);
|
||||
checkTrailing(0x100_0000_0000, 40, 32);
|
||||
checkTrailing(0x8000_0000_0000_0000, 63, 32);
|
||||
|
||||
// 64-bit-range values whose low 32 bits are non-zero: the JS result
|
||||
// is determined entirely by the low half.
|
||||
checkTrailing(0x1_0000_0001, 0, 0);
|
||||
checkTrailing(0x2_0000_0080, 7, 7);
|
||||
|
||||
// Near 2^63, JS doubles only have enough precision for values that
|
||||
// differ by 2048 (= 2^11), so consecutive integers can't all be
|
||||
// represented.
|
||||
// `2^63 + 4096` is exactly representable.
|
||||
// `2^63 + 4095` rounds up to the same value.
|
||||
checkTrailing(0x8000_0000_0000_0000 + 4096, 12, 12);
|
||||
checkTrailing(0x8000_0000_0000_0000 + 4095, 0, 12);
|
||||
}
|
||||
|
||||
void testOneBitCount() {
|
||||
checkOne(0, 0, 0);
|
||||
checkOne(1, 1, 1);
|
||||
checkOne(2, 1, 1);
|
||||
checkOne(3, 2, 2);
|
||||
checkOne(7, 3, 3);
|
||||
checkOne(0x55, 4, 4);
|
||||
checkOne(0xff, 8, 8);
|
||||
checkOne(0xffff_ffff, 32, 32);
|
||||
|
||||
// Negative values: sign-extend to platform width.
|
||||
checkOne(-1, 64, 32);
|
||||
checkOne(-2, 63, 31);
|
||||
checkOne(-3, 63, 31);
|
||||
checkOne(~0x55, 60, 28);
|
||||
checkOne(-0x5555_5555, 49, 17);
|
||||
checkOne(-0x7fff_ffff, 34, 2);
|
||||
checkOne(-0x8000_0000, 33, 1);
|
||||
|
||||
// Values above the 32-bit range. JS counts only the low 32 bits, so
|
||||
// a single bit above bit 31 is invisible to JS oneBitCount.
|
||||
checkOne(0x1_0000_0000, 1, 0);
|
||||
checkOne(0x8000_0000_0000_0000, 1, 0);
|
||||
checkOne(0x2_0000_0001, 2, 1);
|
||||
checkOne(0x1_0000_FFFF, 17, 16);
|
||||
|
||||
// `0x5555_5555_0000_0000 + 0x5555_5555` constructs
|
||||
// 0x5555_5555_5555_5555 on native, but the runtime addition overflows
|
||||
// JS double precision so the result on JS is unpredictable. Only the
|
||||
// native expectation is asserted.
|
||||
if (!jsNumbers) {
|
||||
final pattern = 0x5555_5555_0000_0000 + 0x5555_5555;
|
||||
checkOne(pattern, 32, null);
|
||||
checkOne(~0x8000_0000_0000_0000, 63, null);
|
||||
// Setting any odd-position bit on the alternating pattern should
|
||||
// raise the count from 32 to 33, exercising the 64-bit popcount path
|
||||
// across all positions.
|
||||
for (int i = 1; i < 64; i += 2) {
|
||||
Expect.equals(
|
||||
33,
|
||||
(pattern | (1 << i)).oneBitCount,
|
||||
'(pattern | (1<<$i)).oneBitCount',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exhaustive single-bit coverage across the full platform width.
|
||||
void testSingleBitCoverage() {
|
||||
for (int b = 0; b < width; b++) {
|
||||
final n = 1 << b;
|
||||
Expect.equals(b, n.trailingZeroBitCount, '(1<<$b).trailingZeroBitCount');
|
||||
Expect.equals(1, n.oneBitCount, '(1<<$b).oneBitCount');
|
||||
}
|
||||
}
|
||||
|
||||
// Dart-on-JS guarantees that bit operations performed on unsigned 32-bit
|
||||
// values produce the same answer as on a native 64-bit Dart implementation.
|
||||
// Verify the new getters honor that for a representative set of inputs that
|
||||
// span the full 32-bit unsigned range.
|
||||
void testUnsigned32BitConsistency() {
|
||||
const cases = <(int, int, int)>[
|
||||
(0x0000_0001, 0, 1),
|
||||
(0x0000_0002, 1, 1),
|
||||
(0x0000_0080, 7, 1),
|
||||
(0x0000_FFFF, 0, 16),
|
||||
(0x5555_5555, 0, 16),
|
||||
(0xAAAA_AAAA, 1, 16),
|
||||
(0xCCCC_CCCC, 2, 16),
|
||||
(0xF0F0_F0F0, 4, 16),
|
||||
(0x4000_0000, 30, 1),
|
||||
(0x4000_0001, 0, 2),
|
||||
(0x8000_0000, 31, 1),
|
||||
(0x8000_0001, 0, 2),
|
||||
(0xC000_0000, 30, 2),
|
||||
(0xFFFF_FFFE, 1, 31),
|
||||
(0xFFFF_FFFF, 0, 32),
|
||||
];
|
||||
for (final (n, tzc, obc) in cases) {
|
||||
Expect.equals(tzc, n.trailingZeroBitCount, '$n.trailingZeroBitCount');
|
||||
Expect.equals(obc, n.oneBitCount, '$n.oneBitCount');
|
||||
}
|
||||
}
|
||||
|
||||
void testIdentities() {
|
||||
// n.oneBitCount + (~n).oneBitCount == platform width.
|
||||
for (final n in const [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
7,
|
||||
42,
|
||||
0x7fff_ffff,
|
||||
0x8000_0000,
|
||||
0xffff_ffff,
|
||||
-1,
|
||||
-2,
|
||||
-42,
|
||||
]) {
|
||||
Expect.equals(
|
||||
width,
|
||||
n.oneBitCount + (~n).oneBitCount,
|
||||
'$n.oneBitCount + ~$n.oneBitCount',
|
||||
);
|
||||
}
|
||||
|
||||
// Cross-check: for any nonzero n, `(n & -n) - 1` is a mask of exactly
|
||||
// `trailingZeroBitCount(n)` ones, so counting them recovers that count.
|
||||
// Exercises both getters against each other.
|
||||
void checkIdentity(int n) {
|
||||
Expect.equals(
|
||||
((n & -n) - 1).oneBitCount,
|
||||
n.trailingZeroBitCount,
|
||||
'(($n & -$n) - 1).oneBitCount == $n.trailingZeroBitCount',
|
||||
);
|
||||
}
|
||||
|
||||
// Small values + unsigned 32-bit boundaries + sign-extended negatives.
|
||||
// Identity holds on every backend.
|
||||
for (final n in const [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
7,
|
||||
42,
|
||||
0x4000_0000,
|
||||
0x8000_0000,
|
||||
0xffff_ffff,
|
||||
-1,
|
||||
-2,
|
||||
-42,
|
||||
]) {
|
||||
checkIdentity(n);
|
||||
}
|
||||
|
||||
// Values above the 32-bit range but within JS double mantissa precision
|
||||
// (≤ 2^52). Under Dart's "operate on the low 32 bits" web semantics
|
||||
// both sides of the identity collapse to 32, so the identity still
|
||||
// holds on dart2js / DDC.
|
||||
for (final n in const [
|
||||
0x1_0000_0000, // 2^32
|
||||
0x100_0000_0000, // 2^40
|
||||
0x10_0000_0000_0000, // 2^52
|
||||
]) {
|
||||
checkIdentity(n);
|
||||
}
|
||||
|
||||
// Native-only: values whose source bit pattern is not preserved by JS
|
||||
// doubles. Exercised only on backends with native 64-bit ints. The
|
||||
// `+ 1` form is used to keep `0x20_0000_0000_0000` as the literal,
|
||||
// since the dart2js analyzer rejects literals that can't be
|
||||
// represented exactly as a JS Number.
|
||||
if (!jsNumbers) {
|
||||
checkIdentity(0x20_0000_0000_0000 + 1); // 2^53 + 1
|
||||
checkIdentity(0x8000_0000_0000_0000); // 2^63
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testTrailingZeroBitCount();
|
||||
testOneBitCount();
|
||||
testSingleBitCoverage();
|
||||
testUnsigned32BitConsistency();
|
||||
testIdentities();
|
||||
}
|
||||
Reference in New Issue
Block a user