Skip to content

Commit 1db8615

Browse files
author
Kasper Lund
committed
Avoid just truncating to the nearby int in double.hashCode.
Preserves the property that doubles and ints that are equal share the same hash code, but gives different doubles a much higher chance of having different hash codes. BUG=#2884 Change-Id: Ia97164087db568f26ffbbd6854ed9c19f81e9611 Reviewed-on: https://dart-review.googlesource.com/15140 Commit-Queue: Kasper Lund <[email protected]> Reviewed-by: Vyacheslav Egorov <[email protected]>
1 parent 9aa3a0e commit 1db8615

12 files changed

+277
-8
lines changed

runtime/lib/double.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,31 @@ static RawInteger* DoubleToInteger(double val, const char* error_msg) {
118118
return result.AsValidInteger();
119119
}
120120

121+
DEFINE_NATIVE_ENTRY(Double_hashCode, 1) {
122+
double val = Double::CheckedHandle(arguments->NativeArgAt(0)).value();
123+
if (FLAG_trace_intrinsified_natives) {
124+
OS::Print("Double_hashCode %f\n", val);
125+
}
126+
if (val >= static_cast<double>(kMinInt64) &&
127+
val <= static_cast<double>(kMaxInt64)) {
128+
int64_t ival = static_cast<int64_t>(val);
129+
if (static_cast<double>(ival) == val) {
130+
return Integer::New(ival);
131+
}
132+
} else if (!FLAG_limit_ints_to_64_bits && !isinf(val) && !isnan(val)) {
133+
// Since this code is temporary until we limit ints to 64 bits, we
134+
// reuse the existing DoubleToInteger helper function and pass it
135+
// an empty error message ("") because it cannot fail.
136+
const Integer& bigint = Integer::Handle(DoubleToInteger(val, ""));
137+
if (bigint.AsDoubleValue() == val) {
138+
return bigint.raw();
139+
}
140+
}
141+
142+
uint64_t uval = bit_cast<uint64_t>(val);
143+
return Smi::New(((uval >> 32) ^ (uval)) & kSmiMax);
144+
}
145+
121146
DEFINE_NATIVE_ENTRY(Double_trunc_div, 2) {
122147
double left = Double::CheckedHandle(arguments->NativeArgAt(0)).value();
123148
GET_NON_NULL_NATIVE_ARGUMENT(Double, right_object, arguments->NativeArgAt(1));

runtime/lib/double.dart

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@
77
class _Double implements double {
88
factory _Double.fromInteger(int value) native "Double_doubleFromInteger";
99

10-
// TODO: Make a stared static method for hashCode and _identityHashCode
11-
// when semantics are corrected as described in:
12-
// https://github.com/dart-lang/sdk/issues/2884
13-
int get hashCode => (isNaN || isInfinite) ? 0 : toInt();
14-
int get _identityHashCode => (isNaN || isInfinite) ? 0 : toInt();
10+
int get hashCode native "Double_hashCode";
11+
int get _identityHashCode native "Double_hashCode";
1512

1613
double operator +(num other) {
1714
return _add(other.toDouble());

runtime/vm/bootstrap_natives.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ namespace dart {
7777
V(Developer_log, 8) \
7878
V(Developer_postEvent, 2) \
7979
V(Developer_webServerControl, 2) \
80+
V(Double_hashCode, 1) \
8081
V(Double_getIsNegative, 1) \
8182
V(Double_getIsInfinite, 1) \
8283
V(Double_getIsNaN, 1) \

runtime/vm/compiler/intrinsifier.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,10 @@ bool Intrinsifier::Build_DoubleRound(FlowGraph* flow_graph) {
11471147
return BuildInvokeMathCFunction(&builder, MethodRecognizer::kDoubleRound);
11481148
}
11491149

1150+
void Intrinsifier::Double_identityHash(Assembler* assembler) {
1151+
Double_hashCode(assembler);
1152+
}
1153+
11501154
void Intrinsifier::RegExp_ExecuteMatch(Assembler* assembler) {
11511155
IntrinsifyRegExpExecuteMatch(assembler, /*sticky=*/false);
11521156
}

runtime/vm/compiler/intrinsifier_arm.cc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,6 +1444,53 @@ void Intrinsifier::DoubleToInteger(Assembler* assembler) {
14441444
}
14451445
}
14461446

1447+
void Intrinsifier::Double_hashCode(Assembler* assembler) {
1448+
// TODO(dartbug.com/31174): Convert this to a graph intrinsic.
1449+
1450+
if (!TargetCPUFeatures::vfp_supported()) return;
1451+
1452+
// Load double value and check that it isn't NaN, since ARM gives an
1453+
// FPU exception if you try to convert NaN to an int.
1454+
Label double_hash;
1455+
__ ldr(R1, Address(SP, 0 * kWordSize));
1456+
__ LoadDFromOffset(D0, R1, Double::value_offset() - kHeapObjectTag);
1457+
__ vcmpd(D0, D0);
1458+
__ vmstat();
1459+
__ b(&double_hash, VS);
1460+
1461+
// Convert double value to signed 32-bit int in R0.
1462+
__ vcvtid(S2, D0);
1463+
__ vmovrs(R0, S2);
1464+
1465+
// Tag the int as a Smi, making sure that it fits; this checks for
1466+
// overflow in the conversion from double to int. Conversion
1467+
// overflow is signalled by vcvt through clamping R0 to either
1468+
// INT32_MAX or INT32_MIN (saturation).
1469+
Label fall_through;
1470+
ASSERT(kSmiTag == 0 && kSmiTagShift == 1);
1471+
__ adds(R0, R0, Operand(R0));
1472+
__ b(&fall_through, VS);
1473+
1474+
// Compare the two double values. If they are equal, we return the
1475+
// Smi tagged result immediately as the hash code.
1476+
__ vcvtdi(D1, S2);
1477+
__ vcmpd(D0, D1);
1478+
__ vmstat();
1479+
__ bx(LR, EQ);
1480+
1481+
// Convert the double bits to a hash code that fits in a Smi.
1482+
__ Bind(&double_hash);
1483+
__ ldr(R0, FieldAddress(R1, Double::value_offset()));
1484+
__ ldr(R1, FieldAddress(R1, Double::value_offset() + 4));
1485+
__ eor(R0, R0, Operand(R1));
1486+
__ AndImmediate(R0, R0, kSmiMax);
1487+
__ SmiTag(R0);
1488+
__ Ret();
1489+
1490+
// Fall into the native C++ implementation.
1491+
__ Bind(&fall_through);
1492+
}
1493+
14471494
void Intrinsifier::MathSqrt(Assembler* assembler) {
14481495
if (TargetCPUFeatures::vfp_supported()) {
14491496
Label fall_through, is_smi, double_op;

runtime/vm/compiler/intrinsifier_arm64.cc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,6 +1522,49 @@ void Intrinsifier::DoubleToInteger(Assembler* assembler) {
15221522
__ Bind(&fall_through);
15231523
}
15241524

1525+
void Intrinsifier::Double_hashCode(Assembler* assembler) {
1526+
// TODO(dartbug.com/31174): Convert this to a graph intrinsic.
1527+
1528+
// Load double value and check that it isn't NaN, since ARM gives an
1529+
// FPU exception if you try to convert NaN to an int.
1530+
Label double_hash;
1531+
__ ldr(R1, Address(SP, 0 * kWordSize));
1532+
__ LoadDFieldFromOffset(V0, R1, Double::value_offset());
1533+
__ fcmpd(V0, V0);
1534+
__ b(&double_hash, VS);
1535+
1536+
// Convert double value to signed 64-bit int in R0 and back to a
1537+
// double value in V1.
1538+
__ fcvtzds(R0, V0);
1539+
__ scvtfdx(V1, R0);
1540+
1541+
// Tag the int as a Smi, making sure that it fits; this checks for
1542+
// overflow in the conversion from double to int. Conversion
1543+
// overflow is signalled by fcvt through clamping R0 to either
1544+
// INT64_MAX or INT64_MIN (saturation).
1545+
Label fall_through;
1546+
ASSERT(kSmiTag == 0 && kSmiTagShift == 1);
1547+
__ adds(R0, R0, Operand(R0));
1548+
__ b(&fall_through, VS);
1549+
1550+
// Compare the two double values. If they are equal, we return the
1551+
// Smi tagged result immediately as the hash code.
1552+
__ fcmpd(V0, V1);
1553+
__ b(&double_hash, NE);
1554+
__ ret();
1555+
1556+
// Convert the double bits to a hash code that fits in a Smi.
1557+
__ Bind(&double_hash);
1558+
__ fmovrd(R0, V0);
1559+
__ eor(R0, R0, Operand(R0, LSR, 32));
1560+
__ AndImmediate(R0, R0, kSmiMax);
1561+
__ SmiTag(R0);
1562+
__ ret();
1563+
1564+
// Fall into the native C++ implementation.
1565+
__ Bind(&fall_through);
1566+
}
1567+
15251568
void Intrinsifier::MathSqrt(Assembler* assembler) {
15261569
Label fall_through, is_smi, double_op;
15271570
TestLastArgumentIsDouble(assembler, &is_smi, &fall_through);

runtime/vm/compiler/intrinsifier_ia32.cc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,44 @@ void Intrinsifier::DoubleToInteger(Assembler* assembler) {
15581558
__ Bind(&fall_through);
15591559
}
15601560

1561+
void Intrinsifier::Double_hashCode(Assembler* assembler) {
1562+
// TODO(dartbug.com/31174): Convert this to a graph intrinsic.
1563+
1564+
// Convert double value to signed 32-bit int in EAX and
1565+
// back to a double in XMM1.
1566+
__ movl(ECX, Address(ESP, +1 * kWordSize));
1567+
__ movsd(XMM0, FieldAddress(ECX, Double::value_offset()));
1568+
__ cvttsd2si(EAX, XMM0);
1569+
__ cvtsi2sd(XMM1, EAX);
1570+
1571+
// Tag the int as a Smi, making sure that it fits; this checks for
1572+
// overflow and NaN in the conversion from double to int. Conversion
1573+
// overflow from cvttsd2si is signalled with an INT32_MIN value.
1574+
Label fall_through;
1575+
ASSERT(kSmiTag == 0 && kSmiTagShift == 1);
1576+
__ addl(EAX, EAX);
1577+
__ j(OVERFLOW, &fall_through, Assembler::kNearJump);
1578+
1579+
// Compare the two double values. If they are equal, we return the
1580+
// Smi tagged result immediately as the hash code.
1581+
Label double_hash;
1582+
__ comisd(XMM0, XMM1);
1583+
__ j(NOT_EQUAL, &double_hash, Assembler::kNearJump);
1584+
__ ret();
1585+
1586+
// Convert the double bits to a hash code that fits in a Smi.
1587+
__ Bind(&double_hash);
1588+
__ movl(EAX, FieldAddress(ECX, Double::value_offset()));
1589+
__ movl(ECX, FieldAddress(ECX, Double::value_offset() + 4));
1590+
__ xorl(EAX, ECX);
1591+
__ andl(EAX, Immediate(kSmiMax));
1592+
__ SmiTag(EAX);
1593+
__ ret();
1594+
1595+
// Fall into the native C++ implementation.
1596+
__ Bind(&fall_through);
1597+
}
1598+
15611599
// Argument type is not known
15621600
void Intrinsifier::MathSqrt(Assembler* assembler) {
15631601
Label fall_through, is_smi, double_op;

runtime/vm/compiler/intrinsifier_x64.cc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,45 @@ void Intrinsifier::DoubleToInteger(Assembler* assembler) {
14281428
__ Bind(&fall_through);
14291429
}
14301430

1431+
void Intrinsifier::Double_hashCode(Assembler* assembler) {
1432+
// TODO(dartbug.com/31174): Convert this to a graph intrinsic.
1433+
1434+
// Convert double value to signed 64-bit int in RAX and
1435+
// back to a double in XMM1.
1436+
__ movq(RCX, Address(RSP, +1 * kWordSize));
1437+
__ movsd(XMM0, FieldAddress(RCX, Double::value_offset()));
1438+
__ cvttsd2siq(RAX, XMM0);
1439+
__ cvtsi2sdq(XMM1, RAX);
1440+
1441+
// Tag the int as a Smi, making sure that it fits; this checks for
1442+
// overflow and NaN in the conversion from double to int. Conversion
1443+
// overflow from cvttsd2si is signalled with an INT64_MIN value.
1444+
Label fall_through;
1445+
ASSERT(kSmiTag == 0 && kSmiTagShift == 1);
1446+
__ addq(RAX, RAX);
1447+
__ j(OVERFLOW, &fall_through, Assembler::kNearJump);
1448+
1449+
// Compare the two double values. If they are equal, we return the
1450+
// Smi tagged result immediately as the hash code.
1451+
Label double_hash;
1452+
__ comisd(XMM0, XMM1);
1453+
__ j(NOT_EQUAL, &double_hash, Assembler::kNearJump);
1454+
__ ret();
1455+
1456+
// Convert the double bits to a hash code that fits in a Smi.
1457+
__ Bind(&double_hash);
1458+
__ movq(RAX, FieldAddress(RCX, Double::value_offset()));
1459+
__ movq(RCX, RAX);
1460+
__ shrq(RCX, Immediate(32));
1461+
__ xorq(RAX, RCX);
1462+
__ andq(RAX, Immediate(kSmiMax));
1463+
__ SmiTag(RAX);
1464+
__ ret();
1465+
1466+
// Fall into the native C++ implementation.
1467+
__ Bind(&fall_through);
1468+
}
1469+
14311470
void Intrinsifier::MathSqrt(Assembler* assembler) {
14321471
Label fall_through, is_smi, double_op;
14331472
TestLastArgumentIsDouble(assembler, &is_smi, &fall_through);

runtime/vm/compiler/method_recognizer.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ namespace dart {
161161
V(_Double, -, Double_sub, Double, 0x76768546) \
162162
V(_Double, *, Double_mul, Double, 0x66c66e3d) \
163163
V(_Double, /, Double_div, Double, 0x034b9f08) \
164+
V(_Double, get:hashCode, Double_hashCode, Dynamic, 0x702b0358) \
165+
V(_Double, get:_identityHashCode, Double_identityHash, Dynamic, 0x7bd9e0ea) \
164166
V(_Double, get:isNaN, Double_getIsNaN, Bool, 0x0af9604a) \
165167
V(_Double, get:isInfinite, Double_getIsInfinite, Bool, 0x0f7a56e8) \
166168
V(_Double, get:isNegative, Double_getIsNegative, Bool, 0x3a597395) \
@@ -228,8 +230,7 @@ namespace dart {
228230
V(_IntegerImplementation, >=, Integer_greaterEqualThan, Bool, 0x57491a62) \
229231
V(_IntegerImplementation, <<, Integer_shl, Dynamic, 0x1050c9a8) \
230232
V(_IntegerImplementation, >>, Integer_sar, Dynamic, 0x39af1c69) \
231-
V(_Double, toInt, DoubleToInteger, Dynamic, 0x26ef344b)
232-
233+
V(_Double, toInt, DoubleToInteger, Dynamic, 0x26ef344b) \
233234

234235
#define MATH_LIB_INTRINSIC_LIST(V) \
235236
V(::, sqrt, MathSqrt, Double, 0x70482cf3) \

runtime/vm/simulator_arm64.cc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3125,7 +3125,13 @@ void Simulator::DecodeFPIntCvt(Instr* instr) {
31253125
} else if (instr->Bits(16, 5) == 24) {
31263126
// Format(instr, "fcvtzds'sf 'rd, 'vn");
31273127
const double vn_val = bit_cast<double, int64_t>(get_vregisterd(vn, 0));
3128-
set_register(instr, rd, static_cast<int64_t>(vn_val), instr->RdMode());
3128+
if (vn_val >= static_cast<double>(INT64_MAX)) {
3129+
set_register(instr, rd, INT64_MAX, instr->RdMode());
3130+
} else if (vn_val <= static_cast<double>(INT64_MIN)) {
3131+
set_register(instr, rd, INT64_MIN, instr->RdMode());
3132+
} else {
3133+
set_register(instr, rd, static_cast<int64_t>(vn_val), instr->RdMode());
3134+
}
31293135
} else {
31303136
UnimplementedInstruction(instr);
31313137
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:expect/expect.dart';
6+
7+
main() {
8+
for (int x in [0, 1, 0xffff, 0xffffffff, 0x111111111111, 0xffffffffffff]) {
9+
test(x);
10+
test(-x);
11+
}
12+
13+
// Test with ints outside the 53-bit range that are known to have an
14+
// exact double representation.
15+
test(9007199254840856);
16+
test(144115188075954880);
17+
test(936748722493162112);
18+
}
19+
20+
test(int x) {
21+
Expect.equals(x, x.toDouble().toInt(), "bad test argument ($x)");
22+
Expect.equals(x.hashCode, x.toDouble().hashCode);
23+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// Test that the distribution of hash codes for doubles is reasonable.
6+
7+
import 'package:expect/expect.dart';
8+
9+
main() {
10+
Expect.isTrue(ratio(0, 1) >= 0.95);
11+
Expect.isTrue(ratio(0, 100) >= 0.95);
12+
Expect.isTrue(ratio(0, 0xffffff) >= 0.95);
13+
Expect.isTrue(ratio(0xffffff) >= 0.95);
14+
Expect.isTrue(ratio(0xffffffff) >= 0.95);
15+
Expect.isTrue(ratio(0xffffffffffffff) >= 0.95);
16+
17+
Expect.isTrue(ratio(0, -1) >= 0.95);
18+
Expect.isTrue(ratio(0, -100) >= 0.95);
19+
Expect.isTrue(ratio(0, -0xffffff) >= 0.95);
20+
Expect.isTrue(ratio(-0xffffff) >= 0.95);
21+
Expect.isTrue(ratio(-0xffffffff) >= 0.95);
22+
Expect.isTrue(ratio(-0xffffffffffffff) >= 0.95);
23+
}
24+
25+
double ratio(num start, [num end]) {
26+
final n = 1000;
27+
end ??= (start + 1) * 2;
28+
29+
// Collect the set of distinct doubles and the
30+
// set of distinct hash codes.
31+
final doubles = new Set<double>();
32+
final codes = new Set<int>();
33+
34+
final step = (end.toDouble() - start.toDouble()) / n;
35+
var current = start.toDouble();
36+
for (int i = 0; i < n; i++) {
37+
doubles.add(current);
38+
codes.add(current.hashCode);
39+
current += step;
40+
}
41+
42+
// Return the ratio between distinct doubles and
43+
// distinct hash codes.
44+
return codes.length / doubles.length;
45+
}

0 commit comments

Comments
 (0)