Skip to content

[stdlib][string] Refactors String(repeating:count:) #32369

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

valeriyvan
Copy link
Contributor

Refactors String(repeating:,count:) using String(_uninitializedCapacity:)
Addresses TODO.

Copy link
Contributor

@Catfish-Man Catfish-Man left a comment

Choose a reason for hiding this comment

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

Looks great. Are there any overflow (since we’re using &*) or malloc failure checks in reserveCapacity that we’re now skipping?

@valeriyvan
Copy link
Contributor Author

I just used &* because original code used it.

@valeriyvan valeriyvan changed the title Refactors String(repeating:,count:) [stdlib] Refactors String(repeating:,count:) Jun 14, 2020
@valeriyvan valeriyvan changed the title [stdlib] Refactors String(repeating:,count:) [stdlib][string] Refactors String(repeating:,count:) Jun 14, 2020
@@ -34,13 +34,19 @@ extension String {
return
}

// TODO(String performance): We can directly call appendInPlace
Copy link
Collaborator

Choose a reason for hiding this comment

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

It appears the original author intended to call the internal method appendInPlace, but this PR doesn't do that. Can you explain a bit about how your approach compares to appendInPlace?

for i in 0..<count {
let offset = i * repeatedUTF8.count
let range = offset ..< offset + repeatedUTF8.count
_ = UnsafeMutableBufferPointer(rebasing: buffer[range])
Copy link
Collaborator

@xwu xwu Jun 20, 2020

Choose a reason for hiding this comment

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

Nit: Indentation.

Question: Is there no other way to initialize a chunk of the buffer than to create another mutable buffer pointer and then throw it away?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Question: Is there no other way to initialize a chunk of the buffer than to create another mutable buffer pointer and then throw it away?

UnsafePointer<T> has advanced(by:), which anyway produces UnsafePointer<T> - new pointer.
(Interesting why docs for UnsafePointer misses this method).

I don't see anything like advance().

UnsafeMutableBufferPointer<T> don't have advanced(by:) which might look like a gap in API.

But anyway, it would have produced a new pointer.

The answer for your question: I don't know a way to make it more effective and doubt if such a way exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nit: Indentation.

fixed

Copy link
Collaborator

@xwu xwu Jun 21, 2020

Choose a reason for hiding this comment

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

What about using the raw pointer or raw buffer pointer APIs?

Edit: to be concrete, here's something like what I'm thinking (written freehand, not checked with compiler, could be a silly idea but I'd love to hear what you think):

var repeatedValue = repeatedValue
self = repeatedValue.withUTF8 {
  let source = UnsafeRawBufferPointer($0)
  return String(_uninitializedCapacity: count * source.count) {
    let destination = UnsafeMutableRawBufferPointer($0)
    for i in 0..<count {
      UnsafeMutableRawBufferPointer(
        rebasing: destination[(i &* source.count)...]
      ).copyMemory(from: source)
    }
    return count * source.count
  }
}

Copy link
Contributor Author

@valeriyvan valeriyvan Jun 22, 2020

Choose a reason for hiding this comment

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

Your code and mine both create range and pointer inside loop. Generated code should be more or less the same. The only substantial difference is copyMemory(from:) vs initialise(from:).
if copyMemory(from:) is just a wrapper around memcpy it should be more effective. The more effective the bigger repeatedValue is as I expect memcpy to be be vectorised in C library.

Found out copyMemory(from:) is a wrapper around _memmove(dest:src:size:) which is a wrapper around Builtin.int_memmove_RawPointer_RawPointer_Int64(,,,,).

My guess is - copyMemory(from:) should be faster than initialise(from:) on long enough repeatedValue but will be more or less same fast on very short (several characters) repeatedValue.

I will benchmark this as soon as PR with benchmark is merged.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, and it gets rid of an addition operation in the tight loop since there's no need to calculate the upper bound :)

I also don't know if discarding the return value as you have it here is going to impact the generated code, but perhaps it's a little more elegant not to have to even if it makes no difference in the compiled binary.

(By the way, there's also no need to add to total step by step, as I don't think the copying can fail, so you can return the final value at the end.)

@xwu xwu changed the title [stdlib][string] Refactors String(repeating:,count:) [stdlib][string] Refactors String(repeating:count:) Jun 20, 2020
@valeriyvan
Copy link
Contributor Author

Have added PR #32492 with benchmark

@valeriyvan
Copy link
Contributor Author

ping

@valeriyvan
Copy link
Contributor Author

ping

@xwu
Copy link
Collaborator

xwu commented Jul 26, 2020

Did we ever get the benchmark PR merged? I'd still like to explore the use of raw buffer APIs as discussed above, and I believe we still have a question above that's unanswered, though again I'm not sure who can answer it.

@valeriyvan
Copy link
Contributor Author

Did we ever get the benchmark PR merged?

No. PR #32492 is on review since 22 of June.

I'd still like to explore the use of raw buffer APIs as discussed above, and I believe we still have a question above that's unanswered, though again I'm not sure who can answer it.

How could I be helpful with this?

@valeriyvan valeriyvan requested a review from milseman August 11, 2020 13:15
@shahmishal
Copy link
Member

Please update the base branch to main by Oct 5th otherwise the pull request will be closed automatically.

  • How to change the base branch: (Link)
  • More detail about the branch update: (Link)

@valeriyvan valeriyvan changed the base branch from master to main October 1, 2020 07:31
@valeriyvan
Copy link
Contributor Author

Could we benchmark, please?

@valeriyvan valeriyvan force-pushed the StringRepeating branch 3 times, most recently from ca2748a to 487fb29 Compare March 5, 2023 12:12
@valeriyvan
Copy link
Contributor Author

I am trying to use appendInPlace. Unsuccessfully so far.
What might be wrong in this implementation that it crashes validation test ./validation-test/stdlib/String.swift:

var guts = _StringGuts()
guts.reserveCapacity(repeatedValue._guts.count &* count)
var repeatedValue = repeatedValue
let repeatedValueIsAscii = repeatedValue._guts.isASCII
repeatedValue.withUTF8 { repeatedUTF8 in
  for _ in 0..<count {
    guts.appendInPlace(repeatedUTF8, isASCII: repeatedValueIsAscii)
  }
}
self.init(guts)

As well as following variant, same crash:

var result = String()
result.reserveCapacity(repeatedValue._guts.count &* count)
var repeatedValue = repeatedValue
let repeatedValueIsAscii = repeatedValue._guts.isASCII
repeatedValue.withUTF8 { repeatedUTF8 in
  for _ in 0..<count {
    result._guts.appendInPlace(repeatedUTF8, isASCII: repeatedValueIsAscii)
  }
}
self = result

Here is log:

[ RUN      ] StringTests.StringRepeating/SingleAsciiCharacterCount10
stderr>>> Swift/StringObject.swift:977: Fatal error
stderr>>> Current stack trace:
stderr>>> 0    libswiftCore.dylib                 0x0000000101dc0c48 swift_reportError + 112
stderr>>> 1    libswiftCore.dylib                 0x0000000101e5385c _swift_stdlib_reportFatalErrorInFile + 116
stderr>>> 2    libswiftCore.dylib                 0x0000000101a3ffe4 closure #1 in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) + 236
stderr>>> 3    libswiftCore.dylib                 0x0000000101a3ffe4 closure #1 in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) + 0
stderr>>> 4    libswiftCore.dylib                 0x0000000101a3f8b8 _assertionFailure(_:_:file:line:flags:) + 204
stderr>>> 5    libswiftCore.dylib                 0x0000000101a3fb8c type metadata accessor for IndexingIterator + 0
stderr>>> 6    libswiftCore.dylib                 0x0000000101c36844 closure #1 in String.init(repeating:count:) + 556
stderr>>> 7    libswiftCore.dylib                 0x0000000101a40a88 specialized String.withUTF8<A>(_:) + 124
stderr>>> 8    libswiftCore.dylib                 0x0000000101d758b8 specialized String.init(repeating:count:) + 172
stderr>>> 9    String                             0x00000001006692d0 closure #78 in  + 144
stderr>>> 10   libswiftStdlibUnittest.dylib       0x00000001007a7738 TestSuite._runTest(name:parameter:) + 532
stderr>>> 11   libswiftStdlibUnittest.dylib       0x00000001007a6ad8 _childProcess() + 852
stderr>>> 12   libswiftStdlibUnittest.dylib       0x00000001007b6a0c runAllTests() + 672
stderr>>> 13   String                             0x000000010062aed4 main + 37012
stderr>>> 14   dyld                               0x00000001abf1b460 start + 2544
stderr>>> CRASHED: SIGTRAP
the test crashed unexpectedly
[     FAIL ] StringTests.StringRepeating/SingleAsciiCharacterCount10
[ RUN      ] StringTests.StringRepeating/SingleAsciiCharacterCount1
[       OK ] StringTests.StringRepeating/SingleAsciiCharacterCount1
[ RUN      ] StringTests.StringRepeating/EmptyStringCount10
[       OK ] StringTests.StringRepeating/EmptyStringCount10
[ RUN      ] StringTests.StringRepeating/SingleAsciiCharacterCount0
[       OK ] StringTests.StringRepeating/SingleAsciiCharacterCount0
[ RUN      ] StringTests.StringRepeating/MultipleAsciiCharactersCount2
[       OK ] StringTests.StringRepeating/MultipleAsciiCharactersCount2
[ RUN      ] StringTests.StringRepeating/SingleCyrilicCharacterCount5
stderr>>> Swift/StringObject.swift:977: Fatal error
stderr>>> Current stack trace:
stderr>>> 0    libswiftCore.dylib                 0x00000001045b0c48 swift_reportError + 112
stderr>>> 1    libswiftCore.dylib                 0x000000010464385c _swift_stdlib_reportFatalErrorInFile + 116
stderr>>> 2    libswiftCore.dylib                 0x000000010422ffe4 closure #1 in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) + 236
stderr>>> 3    libswiftCore.dylib                 0x000000010422ffe4 closure #1 in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) + 0
stderr>>> 4    libswiftCore.dylib                 0x000000010422f8b8 _assertionFailure(_:_:file:line:flags:) + 204
stderr>>> 5    libswiftCore.dylib                 0x000000010422fb8c type metadata accessor for IndexingIterator + 0
stderr>>> 6    libswiftCore.dylib                 0x0000000104426844 closure #1 in String.init(repeating:count:) + 556
stderr>>> 7    libswiftCore.dylib                 0x0000000104230a88 specialized String.withUTF8<A>(_:) + 124
stderr>>> 8    libswiftCore.dylib                 0x00000001045658b8 specialized String.init(repeating:count:) + 172
stderr>>> 9    String                             0x0000000102e59a04 closure #83 in  + 152
stderr>>> 10   libswiftStdlibUnittest.dylib       0x0000000102f97738 TestSuite._runTest(name:parameter:) + 532
stderr>>> 11   libswiftStdlibUnittest.dylib       0x0000000102f96ad8 _childProcess() + 852
stderr>>> 12   libswiftStdlibUnittest.dylib       0x0000000102fa6a0c runAllTests() + 672
stderr>>> 13   String                             0x0000000102e1aed4 main + 37012
stderr>>> 14   dyld                               0x00000001abf1b460 start + 2544
stderr>>> CRASHED: SIGTRAP
the test crashed unexpectedly
[     FAIL ] StringTests.StringRepeating/SingleCyrilicCharacterCount5
[ RUN      ] StringTests.StringRepeating/MultipleCyrilicCharactersCount2
[       OK ] StringTests.StringRepeating/MultipleCyrilicCharactersCount2
[ RUN      ] StringTests.StringRepeating/\u{1F1F8}\u{1F1FA}Count2
[       OK ] StringTests.StringRepeating/\u{1F1F8}\u{1F1FA}Count2
[ RUN      ] StringTests.StringRepeating/\u{301}cafeCount5
[       OK ] StringTests.StringRepeating/\u{301}cafeCount5
StringTests: Some tests failed, aborting

@valeriyvan
Copy link
Contributor Author

This variant works:

let repeatedValueGuts = repeatedValue._guts
let buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: repeatedValueGuts.count &* count)
var offset = 0
for _ in 0..<count {
  let bufferRebased = UnsafeMutableBufferPointer(rebasing: buffer[offset...])
  guard let copied = repeatedValueGuts.copyUTF8(into: bufferRebased) else {
    fatalError("Buffer's capacity \(buffer.count)" +
               " is not enough to accommodate '\(repeatedValue)' \(count) times")
  }
  offset += copied
}
 _internalInvariant(offset == buffer.count)
self.init(_StringGuts(UnsafeBufferPointer(buffer), isASCII: repeatedValueGuts.isASCII))

Let's benchmark it?

@valeriyvan
Copy link
Contributor Author

@xwu, may I ask you run benchmark?

@xwu
Copy link
Collaborator

xwu commented Mar 14, 2023

@swift-ci benchmark

@valeriyvan
Copy link
Contributor Author

I have a question ablout buffer deallocation. Snippet above misses defer { buffer.deallocate() }. But if I add it (next line after buffer is allocated), it crashes validation test. What might be wrong?

@valeriyvan
Copy link
Contributor Author

Performance (x86_64): -O

Regression OLD NEW DELTA RATIO
String.initRepeating.longMixStr.Count100 700.333 5081.0 +625.5% 0.14x
Improvement OLD NEW DELTA RATIO
String.initRepeating.1AsciiChar.Count100 506.5 96.059 -81.0% 5.27x
String.initRepeating.26AsciiChar.Count2 24.844 15.347 -38.2% 1.62x
String.initRepeating.33CyrillicChar.Count2 29.946 19.723 -34.1% 1.52x (?)

Code size: -O

Performance (x86_64): -Osize

Regression OLD NEW DELTA RATIO
String.initRepeating.longMixStr.Count100 694.0 4573.0 +558.9% 0.15x
Improvement OLD NEW DELTA RATIO
String.initRepeating.1AsciiChar.Count100 505.0 94.056 -81.4% 5.37x
String.initRepeating.26AsciiChar.Count2 24.731 15.141 -38.8% 1.63x
String.initRepeating.33CyrillicChar.Count2 29.622 19.705 -33.5% 1.50x

Code size: -Osize

Performance (x86_64): -Onone

Regression OLD NEW DELTA RATIO
String.initRepeating.longMixStr.Count100 694.333 4938.0 +611.2% 0.14x
 
Improvement OLD NEW DELTA RATIO
String.initRepeating.1AsciiChar.Count100 508.25 96.389 -81.0% 5.27x
String.initRepeating.26AsciiChar.Count2 25.235 15.74 -37.6% 1.60x (?)
String.initRepeating.33CyrillicChar.Count2 30.324 20.14 -33.6% 1.51x (?)

@valeriyvan
Copy link
Contributor Author

valeriyvan commented Mar 14, 2023

@xwu, may I ask you to run benchmark once again? Thank you.

@valeriyvan
Copy link
Contributor Author

@xwu ping

@xwu
Copy link
Collaborator

xwu commented Apr 21, 2023

@swift-ci benchmark

@valeriyvan valeriyvan requested a review from a team as a code owner June 7, 2025 09:38
@valeriyvan
Copy link
Contributor Author

ping

result += repeatedValue
let bufferRebased = UnsafeMutableBufferPointer(rebasing: buffer[offset...])
guard let copied = repeatedValueGuts.copyUTF8(into: bufferRebased) else {
fatalError("Buffer's capacity \(buffer.count)" +
Copy link
Collaborator

@xwu xwu Jun 29, 2025

Choose a reason for hiding this comment

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

Unless I'm misunderstanding, this is not an error on the end user's part (the buffer capacity is not something under their control) and represents an internal logic error: probably an internal invariant failure is more appropriate here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@xwu
Copy link
Collaborator

xwu commented Jun 30, 2025

@swift-ci smoke test

_internalInvariant(offset == buffer.count)
self = repeatedValueGuts.isASCII ?
String._uncheckedFromASCII(UnsafeBufferPointer(buffer)) :
String._uncheckedFromUTF8(UnsafeBufferPointer(buffer), isASCII: false)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need this ternary logic here, or does _uncheckedFromUTF8 do this anyway using the isASCII argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

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.

8 participants