-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[stdlib] DoubleWidth Implementation #9367
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
Conversation
|
||
public func quotientAndRemainder(dividingBy other: DoubleWidth) | ||
-> (quotient: DoubleWidth, remainder: DoubleWidth) { | ||
let isNegative = (self < DoubleWidth()) != (other < DoubleWidth()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't use 0
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this was from before I implemented the integer literal init—works now for values that aren't larger than an Int
.
} | ||
|
||
public var littleEndian: DoubleWidth<T> { | ||
fatalError() | ||
public var littleEndian: DoubleWidth { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the code for the methods that use byteSwapped
all boilerplate that gets repeated for all integer types, and if so can we factor it out (e.g. into a default implementation)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is! Factored that out into a FixedWidthInteger
default implementation—byteSwapped
is the customization point. I didn't remove the implementations from the standard integer types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not?
Compelling argument! 😂 Those are out now too; tests updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there not performance implications to losing the concrete implementations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would think specialization would take care of that, but I could be wrong…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could write a benchmark, but looking at generated code is probably enough.
on Sun May 07 2017, Nate Cook <notifications-AT-i.8713187.xyz> wrote:
I didn't
remove the implementations from the standard integer types.
Why not?
|
23033e8
to
0dbb5f4
Compare
0dbb5f4
to
5ab34bf
Compare
@swift-ci Please smoke test |
@swift-ci Please smoke test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a thought... Now that the DoubleWidth<T>
has implementation, it is possible and quite interesting to create a 2- or 3-bit FixedWidthInteger
and test DoubleWidth<T>
exhaustively.
do { | ||
let x = UInt1024.max | ||
let (y, o) = x.addingReportingOverflow(1) | ||
expectEqual(y, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: StdlibUnittest
defines expectEquals
for tuples of up to 4 equatable elements.
where Magnitude : BinaryInteger, Magnitude.Magnitude == Magnitude | ||
{ | ||
// FIXME(ABI) (Recursive Protocol Constraints): should add: | ||
// where Magnitude : BinaryInteger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it added 3 lines above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep 🙄
#endif | ||
% end | ||
} | ||
|
||
/// A representation of this integer with the byte order swapped. | ||
public var byteSwapped: ${Self} { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's the basis of all the other endianness conversion operations, probably worth making it @_transparent
or at least @inline(__always)
.
} | ||
|
||
// integer | ||
// | ||
public var magnitude: DoubleWidth<Low> { | ||
if T.isSigned && _storage.high < 0 { | ||
return (DoubleWidth<T>() - self).magnitude | ||
if Base.isSigned && _storage.high < 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_storage.high < (0 as High)
maybe?
} | ||
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary indentation.
} | ||
public func remainderReportingOverflow(dividingBy other: DoubleWidth) | ||
-> (partialValue: DoubleWidth, overflow: ArithmeticOverflow) { | ||
if other == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't remainder overflow in case of .min / -1
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is funny, because the remainder (0) is representable, but the quotient is not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I now remember the discussion about integers whether the remainder should overflow in this case, and I believe we ended up agreeing that it should not.
Regardless, the behavior should be consistent with that of built-in integer types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh! It is a compiler error =) So I guess we stick to the plan. Overflow that is.
(swift) Int8.min.remainderReportingOverflow(dividingBy: -1 as Int8)
<REPL Input>:1:10: error: division '-128 % -1' results in an overflow
Int8.min.remainderReportingOverflow(dividingBy: -1 as Int8)
^
} | ||
% end | ||
|
||
public static func <<=(lhs: inout DoubleWidth, rhs: DoubleWidth) { | ||
if rhs < DoubleWidth((0, 0)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DoubleWidth
should also conform to ExpressibleByIntegerLiteral
right? So this can be replaced by just rhs < (0 as DoubleWidth)
? Not a big win in readability, but at least no ((...))
;-)
} | ||
% end | ||
|
||
public static func <<=(lhs: inout DoubleWidth, rhs: DoubleWidth) { | ||
if rhs < DoubleWidth((0, 0)) { | ||
lhs >>= DoubleWidth((0, 0)) - rhs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefix -
should be able to take care of this as well... I think..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefix -
is only available for signed DoubleWidth
s, so I'm subtracting from zero in non-constrained methods.
|
||
// Shift is exactly the width of `Base`, so low -> high. | ||
if Int(rhs._storage.low) == Base.bitWidth { | ||
lhs = DoubleWidth((High(extendingOrTruncating: lhs._storage.low), numericCast(0))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
numericCast
should not be necessary. Without it the type context is unambiguous, and literal would get the right type... Well, in ideal world, I mean.
} | ||
|
||
// other | ||
// | ||
public init(_builtinIntegerLiteral x: _MaxBuiltinIntegerType) { | ||
fatalError("Method must be overridden") | ||
// FIXME: This won't work if `x` is out of range for `Int` | ||
self.init(Int(_builtinIntegerLiteral: x)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will it change anything if we use Int64
explicitly? I mean, it's not perfect still, but at least on 32-bit platforms it would support larger literals.
Overall @natecook1000 this is AMAZING! Thanks a ton! 👍 |
- Also clean up some 80-column issues - And improve some tests from before literal expressibility
@xwu FWIW, we did not modify our benchmark code at all when the new integers landed. So if there was any degradation in performance it was there and was deemed acceptable. I remember playing with explicit type context in various places, and measuring difference in compilation time as well as performance of the resulting code, and not seeing much difference. The last point you made, though, is really frightening. I'm talking about |
func f<T : BinaryInteger>(_ x: T) -> Bool {
return x != ~0
}
func g<T : BinaryInteger>(_ x: T) -> Bool {
return x != (~0 as T)
}
print(f(UInt.max))
print(g(UInt.max)) Outputs:
And this is bad. 😞 |
@inline(never)
func f<T : BinaryInteger>(_ x: T) -> Bool {
return x != 0
}
@inline(never)
func g<T : BinaryInteger>(_ x: T) -> Bool {
return x != (0 as T)
}
public var result = false
for i in 0 ..< 10_000_000_000 {
result = result || f(i)
// result = result || g(i)
} Rough performance measurements of this code for |
So, this is not simply a problem with heterogeneous comparison, but an interaction with the way literals and overloading work. @DougGregor We may need to have that discussion about literals sooner than we thought. |
@natecook1000 Regarding #9367 (comment) (GH doesn't give me a place to reply), I can't see enough context to guess what assumption you're talking about. |
on Thu May 25 2017, Nate Cook <notifications-AT-i.8713187.xyz> wrote:
A three- or five-bit integer would be particularly good for testing,
since there could easily be power-of-two assumptions in the code. And
like, does a type's `Magnitude` need to have the same bit width as the
type?
OK, I see now what you were referring to. No, a type's Magnitude
doesn't need to have the same bit width, but as Max wrote, it's a good
idea to use a Magnitude of minimal width.
…--
-Dave
|
The assumption is that a fixed width integer's |
on Thu May 25 2017, Nate Cook <notifications-AT-i.8713187.xyz> wrote:
The assumption is that a fixed width integer's `Magnitude` will always
have the same `bitWidth`as the type itself.
The assumption is explicit with `DoubleWidth`, since it uses
`Magnitude` as the lower half.
That relationship needn't be forced, though I suppose it would
complicate overflow detection if that were not the case. In principle,
the compiler should be able to optimize that complication away when the
`bitWidth`'s do match, but that's a lot of code complexity for no gain in
practice.
I think it would be fine to make it a requirement on `DoubleWidth's`
parameter without pushing it into the protocol.
…--
-Dave
|
the heterogeneous comparison with `0 as IntegerLiteralType` is less
performant,
Oh, that is sad. Hopefully that will get fixed before we ship!
…--
-Dave
|
@dabrahams Well, it was a presumption based on reading the code. Maxim has pointed out that the difference is negligible when the type is @inline(never)
func f<T : BinaryInteger>(_ x: T) -> Bool {
return x != 0
}
var result = false
measure {
for i in 0..<1_000_000_000 {
result = result || f(UInt16(i % Int(UInt16.max)))
}
} // 6.962 seconds @inline(never)
func g<T : BinaryInteger>(_ x: T) -> Bool {
return x != (0 as T)
}
var result = false
measure {
for i in 0..<1_000_000_000 {
result = result || g(UInt16(i % Int(UInt16.max)))
}
} // 7.001 seconds |
Fortunately looking at the code isn't a good way to tell 😉 |
On second thought, I'm wrong about the matching |
@swift-ci Please smoke test |
@natecook1000 are you happy with this one or would like to change some more? |
@moiseev This is ready to go! 👍 |
@swift-ci Please Test Source Compatibility |
@swift-ci Please test |
@@ -2361,8 +2395,7 @@ ${operatorComment('&' + x.operator, True)} | |||
//===----------------------------------------------------------------------===// | |||
|
|||
/// An integer type that can represent only nonnegative values. | |||
public protocol UnsignedInteger : BinaryInteger | |||
where Magnitude : BinaryInteger { } | |||
public protocol UnsignedInteger : BinaryInteger { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@natecook1000 I didn't notice this change the first time round. Is there a reason that UnsignedInteger
(and, below, SignedInteger
) are losing the constraint where Magnitude : BinaryInteger
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's now in BinaryInteger
itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep! Constraints on associated types can be recursive as long as the associated type is declared in a higher protocol.
Hashable, Numeric, CustomStringConvertible, Strideable { | ||
|
||
Hashable, Numeric, CustomStringConvertible, Strideable | ||
where Magnitude : BinaryInteger, Magnitude.Magnitude == Magnitude |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xwu here
@swift-ci Please Test Source Compatibility |
[stdlib] DoubleWidth Implementation
cc @moiseev