Skip to content
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

Pool buffers for ivecs and storage refs in the event loop. #2358

Merged
merged 6 commits into from
Feb 7, 2023

Conversation

ser-0xff
Copy link
Contributor

@ser-0xff ser-0xff commented Jan 25, 2023

Pool and reuse IOVector and storage ref buffers in the event pool.

Motivation:

Having an asynchronous sockets backend (like URing on Linux) require extend a lifetime of memory buffers used as arguments for more than a code block.

Modifications:

Event loop have a pool of buffers to be used for output vector write operations.

Result:

In a case of synchronous sockets backend that pool will always have a single buffer, for asynchronous backends every async call will take buffer from the pool and release the buffer when it will not be needed.

@swift-server-bot
Copy link

Can one of the admins verify this patch?

11 similar comments
@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@Lukasa Lukasa added the semver/patch No public API change. label Jan 26, 2023
//
//===----------------------------------------------------------------------===//

public class Pool<Element> {
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't want to be in the business of vending this type as public API. It can safely be moved into NIOPosix.

private let dtor: (Element) -> Void
private var elements: [Element]

public init(maxSize: Int, ctor: @escaping () -> Element, dtor: @escaping (Element) -> Void) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than using separate constructor/destructor functions, we'd typically want to define a protocol to which Element conforms that implements the relevant methods. Something like:

protocol PoolElement {
    init()

    func evictedFromPool()
}

Copy link
Contributor Author

@ser-0xff ser-0xff Jan 27, 2023

Choose a reason for hiding this comment

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

I had some debates regarding such approach which is I see is common for swift, but sometimes it does not work well from my point of view. So we are going to extend some type with 'PoolElement' protocol. But is it really needed? Why the particular type should expose the 'PoolElement' functionality and why it should even know that it is going to be pooled with some pool? Often it is only the particular pool who knows what does it going to pool and how... As you can see in the PR there are 2 pools:

  • pool to be used in the event loop, which just allocates buffer
  • pool to be used in tests, which allocates buffers with markers and check those markers when buffer is not needed any more...

The PoolElement protocol does not give a possibility to have a different pooling logic for the same type in different pools... or at least I can't see a simple way for that...
Or am I missing something?

Copy link
Contributor

Choose a reason for hiding this comment

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

The thing you're missing is that the way to pool the "same thing" in different ways is to define a new type with the new behaviour. Swift structs are essentially "free": they're trivial aggregates. That means whenever we want a different behaviour we can wrap the type we want in a new structure that adds that new behaviour.

In general if we were defining a public API I'd be a bit more cautious, but as this is purely an internal data structure I think it's probably nicer to avoid unstructured callbacks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, I made another branch with a Pool<> which operates with something implementing 'PoolElement'.
I did a PoolableBuffer struct implementing default behaviour: https://github.com/ordo-one/swift-nio/blob/aa23e675eb95292f98c08391cf30bec37fed8292/Sources/NIOPosix/SelectableEventLoop.swift#L33
My understanding was we can define a different behaviour for a poolable buffer adding some bounds checks, like I did at https://github.com/ordo-one/swift-nio/blob/aa23e675eb95292f98c08391cf30bec37fed8292/Tests/NIOPosixTests/ChannelTests.swift#L76, but TestPooledBuffer can not be used as a pooled type, the code does not compile...
Am I missing something or did I get you wrong?

Copy link
Contributor

Choose a reason for hiding this comment

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

When you say the code doesn't compile, what did you try to do and what happened?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I pushed a commit with some bounds checks.
a437a52
Could you have a look?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks great!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you think we can merge these changes?
What will be better, create a new PR from that branch, or apply those changes to the branch where that PR is made from?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can merge those changes into this branch.

Copy link
Contributor Author

@ser-0xff ser-0xff Feb 2, 2023

Choose a reason for hiding this comment

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

Branch pool-iovecs-buffers3 was created from the swift-nio/main, not from the pool-iovecs-buffers, so it would be merged with conflicts.
I applied changes we discussed to the pool-iovecs-buffers, please have a look.

}

deinit {
while !elements.isEmpty {
Copy link
Contributor

Choose a reason for hiding this comment

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

This can just be a for loop, we don't need to deinit the elements immediately.

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


let count = (buffer.count / (MemoryLayout<IOVector>.stride + MemoryLayout<Unmanaged<AnyObject>>.stride))
let iovecs = (buffer.baseAddress!).bindMemory(to: IOVector.self, capacity: count)
let storageRefs = (buffer.baseAddress! + (count * MemoryLayout<IOVector>.stride)).bindMemory(to: Unmanaged<AnyObject>.self, capacity: count)
Copy link
Contributor

Choose a reason for hiding this comment

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

This code is very precise. Can we factor it out into its own method to which we can add assertions on appropriate length?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure I got you...
what kind of assertion would you like to add here?

Copy link
Contributor

Choose a reason for hiding this comment

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

So what you're doing here is allocating a single buffer that you then slice into the relevant buffers we want. That's fine, but I'd like to encapsulate all of that dance into a struct that we can use to hide this mess of memory binding. It will also let us add a bunch of pseudo-bounds-checks to our construction code, so we can confirm that we're not actually binding or using memory that is out of bounds of the original allocation. Does that make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well,
I would look at that with a very simple metric: number of code lines and number of extra entities (we will need to introduce an additional structure). Here we have just 3 lines of code, adding a struct with that functionality will give more than 3 lines... But if you think it is better in that particular case - let's do it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Generally I don't think lines of code added is the particular metric I'm most concerned about. What I want is to build abstractions that discourage misuse. A related opportunity is to actively reference count the pointers, but I think this is a one-step-at-a-time kind of deal, so I'm interested for now in giving us a sensible abstraction.


init() {
self.bufferSize = (MemoryLayout<IOVector>.stride + MemoryLayout<Unmanaged<AnyObject>>.stride) * Socket.writevLimitIOVectors
#if DEBUG
Copy link
Contributor

Choose a reason for hiding this comment

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

We have a debugOnly helper you can use here.

private let buffer: UnsafeMutableRawPointer

init() {
self.bufferSize = (MemoryLayout<IOVector>.stride + MemoryLayout<Unmanaged<AnyObject>>.stride) * Socket.writevLimitIOVectors
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add a precondition that MemoryLayout<IOVector>.alignment >= MemoryLayout<Unmanaged<AnyObject>>.alignment? It'll allow us to confirm that this arrangement actually works.

}

func get() -> (UnsafeMutableBufferPointer<IOVector>, UnsafeMutableBufferPointer<Unmanaged<AnyObject>>) {
let count = self.bufferSize / (MemoryLayout<IOVector>.stride + MemoryLayout<Unmanaged<AnyObject>>.stride)
Copy link
Contributor

Choose a reason for hiding this comment

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

This number is Socket.writevLimitIOVectors, right?

}
}

public func put(_ e: Element) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: let's strip the publics.

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 all above.

@Lukasa
Copy link
Contributor

Lukasa commented Feb 2, 2023

@swift-server-bot test this please

assert(iovecs <= (self.buffer + self.bufferSize).bindMemory(to: IOVector.self, capacity: count))
assert(storageRefs >= self.buffer.bindMemory(to: Unmanaged<AnyObject>.self, capacity: count))
assert(storageRefs <= (self.buffer + self.bufferSize).bindMemory(to: Unmanaged<AnyObject>.self, capacity: count))
assert(UnsafeMutableRawPointer(iovecs + count) == UnsafeMutableRawPointer(storageRefs))
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't bind the memory in these assertions, the duplicate bind makes it hard to reason about. Erase the pointers to raw instead.

Copy link
Contributor Author

@ser-0xff ser-0xff Feb 3, 2023

Choose a reason for hiding this comment

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

It does not compile with swift 5.5/5.6 then, CI works fails...
We could remove bindings but will need to fix a build for 5.5/5.6 in some other way then...
#if swift(>=5.7)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

or do you mean having it like

        assert(UnsafeMutableRawPointer(iovecs) >= self.buffer)
        assert(UnsafeMutableRawPointer(iovecs) <= (self.buffer + self.bufferSize))
        assert(UnsafeMutableRawPointer(storageRefs) >= self.buffer)
        assert(UnsafeMutableRawPointer(storageRefs) <= (self.buffer + self.bufferSize))
        assert(UnsafeMutableRawPointer(iovecs + count) == UnsafeMutableRawPointer(storageRefs))
        assert(UnsafeMutableRawPointer(storageRefs + count) <= (self.buffer + bufferSize))

@Lukasa
Copy link
Contributor

Lukasa commented Feb 3, 2023

@swift-server-bot test this please

2 similar comments
@Lukasa
Copy link
Contributor

Lukasa commented Feb 3, 2023

@swift-server-bot test this please

@Lukasa
Copy link
Contributor

Lukasa commented Feb 6, 2023

@swift-server-bot test this please

@Lukasa
Copy link
Contributor

Lukasa commented Feb 7, 2023

@swift-server-bot test this please

@Lukasa
Copy link
Contributor

Lukasa commented Feb 7, 2023

@swift-server-bot test this please

@ser-0xff ser-0xff marked this pull request as ready for review February 7, 2023 11:02
Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

For the sake of keepings things moving forward, I'm happy for us to merge this as-is. However, I'm going to provide a follow-up PR myself to refactor this a bit to make it a little safer. In particular, I want us to avoid the risk of leaking memory, so I'm going to change the layout of the PoolElement somewhat.

@Lukasa Lukasa merged commit 1e7ad9a into apple:main Feb 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants