Skip to content

[stdlib] non-mutating range subscript setter on UnsafeMutableBufferPointer #12504

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

Merged
merged 4 commits into from
Mar 6, 2018

Conversation

glessard
Copy link
Contributor

@glessard glessard commented Oct 19, 2017

In an apparent oversight (?), the setter for UnsafeMutableBufferPointer subscript(bounds: Range<Int>) isn't marked as nonmutating. Given that UnsafeMutableRawBufferPointer's equivalent is nonmutating, here is a non-mutating implementation.

I believe this shouldn't require swift-evolution involvement, but if this gets merged some new "un-mutated var" warnings may appear.

SR-3782 mentions this. Someone mentioned working on it months ago, but there did not appear to be a pull request.

@glessard glessard force-pushed the umbp-nonmutating-setter branch 3 times, most recently from c6354c9 to 479c94b Compare October 19, 2017 11:20
@glessard glessard changed the title a non-mutating range subscript setter on UnsafeMutableBufferPointer [stdlib] non-mutating range subscript setter on UnsafeMutableBufferPointer Oct 19, 2017
@glessard glessard force-pushed the umbp-nonmutating-setter branch 2 times, most recently from 4223418 to d72b1ac Compare October 20, 2017 17:34
Copy link
Member

@airspeedswift airspeedswift left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking this up! Couple of comments inline. This also needs some tests confirming it's working as expected against immutable buffer variables (and tests in general, if that isn't already isn't already covered by existing unit tests).

// FIXME: swift-3-indexing-model: tests.
_writeBackMutableSlice(&self, bounds: bounds, slice: newValue)
if newValue.count > 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be !newValue.isEmpty. I realize it's not a big deal in this case since we're talking about integer-indexed collections here but we should never use count to check for emptiness.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to change UnsafeMutableRawBufferPointer implementation as well.

if newValue.count > 0 {
var sliceIndex = newValue.startIndex
var index = bounds.lowerBound
while index != bounds.upperBound && sliceIndex != newValue.endIndex {
Copy link
Member

@airspeedswift airspeedswift Oct 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we know these are two unsafe buffers (unlike _writeBackMutableSlice , which is generic) we ought to be able to just do a straight memory copy, once we've checked the count is the same. Something like...

  baseAddress!.initialize(from: newValue.base.baseAddress!, count: bounds.count)

The only thing that would worry me about if this is correct is the possibility of overlapping assignment from self...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnsafeMutablePointer.assign supports overlapping ranges. That's the thing to use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly but use assignment.
baseAddress!.assign(from: newValue.base.baseAddress!, count: bounds.count)

That does the correct forward or backward copy depending on the relative pointer positions.

_debugPrecondition(bounds.lowerBound >= startIndex)
_debugPrecondition(bounds.upperBound <= endIndex)
_debugPrecondition(bounds.count == newValue.count)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one should be a full _precondition (similar to the one found in _writeBackMutableSlice).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The analog line in UnsafeRawBufferPointer uses a _debugPrecondition: https://github.com/apple/swift/blob/ffd34239a1b6f046346769a55ae8e717f64e13eb/stdlib/public/core/UnsafeRawBufferPointer.swift.gyb#L468
Is there a reason to make it stronger in the typed buffers, or should it be a _precondition in both places?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasoning behind _debugPrecondition is that this is already an Unsafe type and being at the the lowest level of abstraction will be used in performance critical code.

@airspeedswift airspeedswift requested a review from atrick October 26, 2017 15:01
/// the range must be valid indices of the buffer.
///
%if Mutable:
/// - Complexity: O(1) for reading, O(N) for writing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see where the complexity bounds comes from, but I think it's misleading. It subtly reflects the fact that "reading" is deferred until the slice is used. And N will likely be mistaken for the buffer count. Since there's nothing magic or unexpected happening here, why talk about complexity? @airspeedswift, is this something we need to document?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same complexity that's documented at the protocol level, so we can remove it here. We only document on types when there's a deviation from the expectation (like Set.count).

/// the range must be valid indices of the buffer.
///
%if Mutable:
/// - Complexity: O(1) for reading, O(N) for writing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same complexity that's documented at the protocol level, so we can remove it here. We only document on types when there's a deviation from the expectation (like Set.count).

%if Mutable:
/// var streets = ["Adams", "Bryant", "Channing", "Douglas", "Evarts"]
/// streets.withUnsafeMutableBufferPointer { buffer in
/// let streetSlice = buffer[2..<buffer.endIndex]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adapting these docs! Just to keep everyone on their toes, we use four-space indentation in documentation examples, instead of the two used in code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh? I hadn't noticed!

@glessard
Copy link
Contributor Author

Regarding tests: I had assumed that when the tests passed it had been covered.
However, this is covered in validation-tests, but not in the regular tests. UnsafeRawBufferPointer has a test suite under tests/stdlib; it could be adapted for UnsafeBufferPointer, or this would remain covered during validation only. What's the recommendation?

@atrick
Copy link
Contributor

atrick commented Oct 26, 2017

UnsafeRawBufferPointer has a separate set of tests that cover behavior specific to that type, like non-Collection APIs and debug bounds checks. Unlike the validation test, they are not meant to be exhaustive. UnsafeBufferPointer could probably have the same thing if you think it's useful. I don't have a strong opinion.

@airspeedswift
Copy link
Member

I just realized, the setter should account for the fact that a mutation of self doesn't need to actually be written back, because reference semantics mean it's already happened.

i.e.:

someUnsafeBuffer[subrange].sort()

will generate first a call to get, then sort the slice, then write the sorted slice back via set. But when the type has reference semantics, the write-back is unnecessary, because the mutation has already happened on the underlying data, via the slice. So the non mutating set should check the identity of the buffer pointer to check for this scenario.

@glessard
Copy link
Contributor Author

Is the pointer identity not checked by the builtin function called by assign(from:count:)?

count: newValue.count)
let source = newValue.base._position! + newValue.startIndex
let destination = _position! + bounds.lowerBound
if source != destination {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's appropriate to do this check, is there a reason not to do it directly as an else if in assign?
https://github.com/apple/swift/blob/e4236405279f42345e12eebbc023a3409a034b7a/stdlib/public/core/UnsafePointer.swift.gyb#L562-L564

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glessard
Copy link
Contributor Author

glessard commented Oct 27, 2017

UnsafeMutableBufferPointer and UnsafeMutableRawBufferPointer both need a non-mutating public subscript<R: RangeExpression>(r: R) (and perhaps public subscript(x: UnboundedRange)). Is that an evolution topic?

@atrick
Copy link
Contributor

atrick commented Oct 27, 2017

Is the pointer identity not checked by the builtin function called by assign(from:count:)?

Possibly somewhere in the runtime or libc. The point is, we don't even want to make that series of runtime calls. Checking in the else case of assign seems reasonable to me. Maybe that should also be in the doc comment since we want to make that assumption when called from UnsafeMutableBufferPointer.

@glessard glessard force-pushed the umbp-nonmutating-setter branch from 7db7a1c to a9daabd Compare October 27, 2017 22:53
@atrick
Copy link
Contributor

atrick commented Oct 28, 2017

👍

@airspeedswift
Copy link
Member

@swift-ci please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - b4bc6d3a2e0cbbb0ad91ab3ce46a002768fd16f4

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - b4bc6d3a2e0cbbb0ad91ab3ce46a002768fd16f4

@glessard
Copy link
Contributor Author

The CollectionType validation test is expecting a particular crash message in withUnsafeMutableBufferPointer for the debug precondition
_debugPrecondition(bounds.count == newValue.count)
I'm not sure whether to just copy/paste the message or change the expectation...

@atrick
Copy link
Contributor

atrick commented Oct 30, 2017

The addition of debug messages seems fine to me.

@glessard
Copy link
Contributor Author

Upon looking at it again, the validation tests that failed are called "subscript(_: Range)/defaultImplementation". They currently work by modifying an array through withUnsafeMutableBufferPointer in order to get a non-range-replaceable MutableCollection, but they're not testing the default implementation (_writeBackMutableSlice) anymore. I'm looking for a different MutableCollection they could call through.

@glessard glessard force-pushed the umbp-nonmutating-setter branch from e8869e2 to 26b3de1 Compare October 31, 2017 04:57
@glessard glessard force-pushed the umbp-nonmutating-setter branch 2 times, most recently from 8f7daf7 to 24ab86c Compare November 9, 2017 19:49
@glessard glessard force-pushed the umbp-nonmutating-setter branch from 24ab86c to b0f66f8 Compare November 9, 2017 19:51
@glessard
Copy link
Contributor Author

glessard commented Nov 9, 2017

Squashed commits. Do we want fewer still?

@atrick
Copy link
Contributor

atrick commented Nov 9, 2017

Either way, 1 commit or 4 commits, is fine with me.

@glessard
Copy link
Contributor Author

glessard commented Nov 9, 2017

OK then; to my eye it seems ready to merge.

@airspeedswift
Copy link
Member

@swift-ci please test

@airspeedswift
Copy link
Member

@swift-ci please test

@swift-ci
Copy link
Contributor

swift-ci commented Dec 2, 2017

Build failed
Swift Test Linux Platform
Git Sha - b0f66f835c08d8e009973ac7937d6235779c4c94

@airspeedswift
Copy link
Member

@swift-ci please test source compatibility

@swift-ci
Copy link
Contributor

swift-ci commented Dec 2, 2017

Build failed
Swift Test OS X Platform
Git Sha - b0f66f835c08d8e009973ac7937d6235779c4c94

@airspeedswift
Copy link
Member

@swift-ci please smoke test compiler performance

@swift-ci
Copy link
Contributor

swift-ci commented Dec 2, 2017

Build comment file:

Summary for master smoketest

No regressions above thresholds

Debug

debug brief

Regressed (0)
name old new delta delta_pct
Improved (0)
name old new delta delta_pct
Unchanged (delta < 1.0% or delta < 100.0ms) (2)
name old new delta delta_pct
LLVM.NumLLVMBytesOutput 23,285,028 23,285,020 -8 -0.0%
time.swift-driver.wall 30.9s 30.8s -77.2ms -0.25%

debug detailed

Regressed (0)
name old new delta delta_pct
Improved (0)
name old new delta delta_pct
Unchanged (delta < 1.0% or delta < 100.0ms) (23)
name old new delta delta_pct
AST.NumImportedExternalDefinitions 47,780 47,780 0 0.0%
AST.NumLoadedModules 6,369 6,369 0 0.0%
AST.NumTotalClangImportedEntities 133,186 133,186 0 0.0%
AST.NumUsedConformances 2,713 2,713 0 0.0%
IRModule.NumIRBasicBlocks 65,495 65,495 0 0.0%
IRModule.NumIRFunctions 32,935 32,935 0 0.0%
IRModule.NumIRGlobals 36,442 36,442 0 0.0%
IRModule.NumIRInsts 702,158 702,158 0 0.0%
IRModule.NumIRValueSymbols 59,725 59,725 0 0.0%
LLVM.NumLLVMBytesOutput 23,285,028 23,285,020 -8 -0.0%
SILModule.NumSILGenFunctions 14,638 14,638 0 0.0%
SILModule.NumSILOptFunctions 25,709 25,707 -2 -0.01%
Sema.NumConformancesDeserialized 114,967 114,967 0 0.0%
Sema.NumConstraintScopes 416,017 416,017 0 0.0%
Sema.NumDeclsDeserialized 906,521 906,521 0 0.0%
Sema.NumDeclsValidated 28,449 28,449 0 0.0%
Sema.NumFunctionsTypechecked 27,343 27,343 0 0.0%
Sema.NumGenericSignatureBuilders 36,829 36,829 0 0.0%
Sema.NumLazyGenericEnvironments 185,054 185,054 0 0.0%
Sema.NumLazyGenericEnvironmentsLoaded 18,665 18,665 0 0.0%
Sema.NumLazyIterableDeclContexts 152,672 152,672 0 0.0%
Sema.NumTypesDeserialized 967,524 967,486 -38 -0.0%
Sema.NumTypesValidated 97,068 97,068 0 0.0%

Release

release brief

Regressed (0)
name old new delta delta_pct
Improved (0)
name old new delta delta_pct
Unchanged (delta < 1.0% or delta < 100.0ms) (2)
name old new delta delta_pct
LLVM.NumLLVMBytesOutput 28,803,372 28,803,372 0 0.0%
time.swift-driver.wall 66.8s 66.8s -24.5ms -0.04%

release detailed

Regressed (0)
name old new delta delta_pct
Improved (0)
name old new delta delta_pct
Unchanged (delta < 1.0% or delta < 100.0ms) (23)
name old new delta delta_pct
AST.NumImportedExternalDefinitions 7,421 7,421 0 0.0%
AST.NumLoadedModules 227 227 0 0.0%
AST.NumTotalClangImportedEntities 22,601 22,601 0 0.0%
AST.NumUsedConformances 2,719 2,719 0 0.0%
IRModule.NumIRBasicBlocks 61,475 61,475 0 0.0%
IRModule.NumIRFunctions 26,597 26,597 0 0.0%
IRModule.NumIRGlobals 34,113 34,113 0 0.0%
IRModule.NumIRInsts 581,886 581,886 0 0.0%
IRModule.NumIRValueSymbols 54,292 54,292 0 0.0%
LLVM.NumLLVMBytesOutput 28,803,372 28,803,372 0 0.0%
SILModule.NumSILGenFunctions 10,842 10,842 0 0.0%
SILModule.NumSILOptFunctions 19,650 19,655 5 0.03%
Sema.NumConformancesDeserialized 68,558 68,558 0 0.0%
Sema.NumConstraintScopes 395,763 395,763 0 0.0%
Sema.NumDeclsDeserialized 133,769 133,769 0 0.0%
Sema.NumDeclsValidated 17,632 17,632 0 0.0%
Sema.NumFunctionsTypechecked 7,471 7,471 0 0.0%
Sema.NumGenericSignatureBuilders 4,540 4,540 0 0.0%
Sema.NumLazyGenericEnvironments 23,679 23,679 0 0.0%
Sema.NumLazyGenericEnvironmentsLoaded 2,660 2,660 0 0.0%
Sema.NumLazyIterableDeclContexts 12,645 12,645 0 0.0%
Sema.NumTypesDeserialized 157,620 157,601 -19 -0.01%
Sema.NumTypesValidated 31,082 31,082 0 0.0%

@glessard glessard force-pushed the umbp-nonmutating-setter branch from e2b8520 to 0fb0d51 Compare December 8, 2017 21:18
@@ -580,21 +580,17 @@ CollectionTypeTests.test("subscript(_: Range<Index>)/writeback") {
CollectionTypeTests.test("subscript(_: Range<Index>)/defaultImplementation/sliceTooLarge")
.crashOutputMatches("Cannot replace a slice of a MutableCollection with a slice of a larger size")
.code {
var x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var x = Slice(base: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], bounds: 0..<10)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed from MutableSlice to Slice. Conditional Conformances are nice.

@lorentey
Copy link
Member

@swift-ci please test

@glessard
Copy link
Contributor Author

glessard commented Mar 6, 2018

@atrick Sorry to bother, but is this PR waiting on something else I'm not aware of?

@atrick
Copy link
Contributor

atrick commented Mar 6, 2018

@glessard It looks like I was the first to give this a thumbs up but was waiting for @airspeedswift review. @lorentey, you last ran a successful test here, do you want to retest and merge?

@lorentey
Copy link
Member

lorentey commented Mar 6, 2018

@swift-ci please test and merge

@swift-ci swift-ci merged commit abbaa47 into swiftlang:master Mar 6, 2018
@glessard
Copy link
Contributor Author

glessard commented Mar 6, 2018

Thanks

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.

6 participants