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

enhance SubmissionQueue to support generate SQE in place #119

Closed
wants to merge 7 commits into from

Conversation

jiangliu
Copy link

@jiangliu jiangliu commented Dec 1, 2021

Enhance io_uring with new interfaces to support prepare SQE in place. These new interfaces pass argument by reference instead of by value, which then could be used to support ringbahn.

  1. introduce trait PrepareSQE for opcode
  2. change opcode to implement the PrepareSQE trait
  3. add SubmissionQueue::push_command()/push_commands() to generate SQE in place.

I have add some unit test cases/one benchmark case. According to my test result, there's no regression for existing SubmissionQueue::push(). The new SubmissionQueue::push_command() is slightly slower than push() by acceptable.

@quininer
Copy link
Member

quininer commented Dec 1, 2021

Can you describe what problem it solves?

@jiangliu
Copy link
Author

jiangliu commented Dec 2, 2021

Can you describe what problem it solves?

Sorry, I has misused an old version as work base, and found a bug in the old version and it's fixed in the latest code base.
BTW, I have tried to follow the way as you described, but feels it's a little complex to support push_multiple_opcode(). So I have slightly changed the design and post it here as RFC.
There are still more changes needed for opcode.rs, I will handle it once we have agreed on the design:)

@jiangliu jiangliu changed the title [WIP] Enhance io-uring for safety [RFC] enhance submission queue to generate SQE in place Dec 2, 2021
@jiangliu jiangliu changed the title [RFC] enhance submission queue to generate SQE in place [RFC] enhance SubmissionQueue to support generate SQE in place Dec 2, 2021
@quininer
Copy link
Member

quininer commented Dec 2, 2021

The RefCell<()> added in this PR has nothing to do with our previous discussion and destroys the meaning of borrow_shared.

The borrow_shared is an unsafe api that requires the caller to ensure safety by themselves. It is used to implement cross-threaded apis such as owned split.

@jiangliu
Copy link
Author

jiangliu commented Dec 2, 2021

The RefCell<()> added in this PR has nothing to do with our previous discussion and destroys the meaning of borrow_shared.

The borrow_shared is an unsafe api that requires the caller to ensure safety by themselves. It is used to implement cross-threaded apis such as owned split.

Sure, I started working by reading all the code and then found something interesting:)
I will split that patch into dedicated thread.
BTW, ownedsplit is great future and definitely helps enabling rust async io with io uring.
I'm working on combing ringbahn and io-uring crates togetther:)

@jiangliu
Copy link
Author

jiangliu commented Dec 2, 2021

BTW, I failed to run "cargo run --package io-uring-test" with the latest code with an error:

  = note: /uring/target/debug/deps/io_uring_test-af1e753fdc660fe4.3x7eimgxzg68dtpw.rcgu.o: In function `io_uring_test::tests::fs::test_statx':
          /uring/io-uring-test/src/tests/fs.rs:316: undefined reference to `statx'
          collect2: error: ld returned 1 exit status

And the toolchain used is "nightly-x86_64-unknown-linux-gnu".
Anything special needed to run the test crate?

@quininer
Copy link
Member

quininer commented Dec 2, 2021

You may be using a distribution that does not support statx, see here.

@jiangliu
Copy link
Author

jiangliu commented Dec 2, 2021

The RefCell<()> added in this PR has nothing to do with our previous discussion and destroys the meaning of borrow_shared.

The borrow_shared is an unsafe api that requires the caller to ensure safety by themselves. It is used to implement cross-threaded apis such as owned split.

Aha, the RefCell actually helps ownedsplit. Say:

{
       let uring = IoUring::new();
       let (submit, sq, cq) = uring.split();
       let sq2 = sq.clone();
       let q1 = sq. submission();
       // With the RefCell changes, following statement should panic, which should be the expected behavior:)
       let q2 = sq2.submission();
}

@jiangliu
Copy link
Author

jiangliu commented Dec 2, 2021

The RefCell<()> added in this PR has nothing to do with our previous discussion and destroys the meaning of borrow_shared.
The borrow_shared is an unsafe api that requires the caller to ensure safety by themselves. It is used to implement cross-threaded apis such as owned split.

Aha, the RefCell actually helps ownedsplit. Say:

{
       let uring = IoUring::new();
       let (submit, sq, cq) = uring.split();
       let sq2 = sq.clone();
       let q1 = sq. submission();
       // With the RefCell changes, following statement should panic, which should be the expected behavior:)
       let q2 = sq2.submission();
}

My fault, SubmissionUring does not support Clone yet, so it should be safe.

@jiangliu jiangliu changed the title [RFC] enhance SubmissionQueue to support generate SQE in place enhance SubmissionQueue to support generate SQE in place Dec 8, 2021
@jiangliu
Copy link
Author

Hi @quininer , I have updated the MR and feel it's ready for review:)

@quininer
Copy link
Member

I missed it and I will look at it on the weekend.

Copy link
Member

@quininer quininer left a comment

Choose a reason for hiding this comment

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

I am a little bit dissatisfied with the api proposed by this PR. although it can indeed avoid build io_uring_sqe, but it cannot avoid need to build Opcode type once.

benchmarks show that it does not have many performance advantages compared to normal push. I am not sure if this change is worthwhile.

normal                  time:   [6.0094 us 6.0865 us 6.1785 us]
                        change: [-0.1695% +1.3807% +3.1320%] (p = 0.11 > 0.05)
                        No change in performance detected.

prepare                 time:   [6.3320 us 6.4818 us 6.6643 us]
                        change: [-8.5613% -5.6659% -2.6755%] (p = 0.00 < 0.05)
                        Performance has improved.

src/squeue.rs Outdated Show resolved Hide resolved
src/squeue.rs Outdated
@@ -285,6 +354,44 @@ impl Entry {
}
}

impl OptionValues {
Copy link
Member

Choose a reason for hiding this comment

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

The name should be more descriptive.

Copy link
Author

Choose a reason for hiding this comment

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

How about "SqeCommonOptions"?

src/squeue.rs Show resolved Hide resolved
src/squeue.rs Outdated Show resolved Hide resolved
src/opcode.rs Outdated
/// Trait to prepare an SQE from an opcode object.
pub trait PrepareSQE {
/// Prepare an SQE from an opcode object.
fn prepare(&self, sqe: &mut sys::io_uring_sqe, options: Option<&OptionValues>);
Copy link
Member

Choose a reason for hiding this comment

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

It should not expose internal types, maybe this trait can not be exposed。

Copy link
Author

@jiangliu jiangliu Jan 2, 2022

Choose a reason for hiding this comment

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

Both push_command() and push_commands() uses PrepareSQE as generic types.

@jiangliu
Copy link
Author

jiangliu commented Jan 3, 2022

@quininer I have implemented a version to prepare SQE in place. And take Readv as an example, the expanded code are as below:

    /// Vectored read, equivalent to `preadv2(2)`.
    pub struct Readv {
        fd: sealed::Target,
        iovec: *const libc::iovec,
        len: u32,
        ioprio: u16,
        offset: libc::off_t,
        rw_flags: types::RwFlags,
    }

    impl Readv {
        /// The opcode of the operation. This can be passed to
        /// [`Probe::is_supported`](crate::Probe::is_supported) to check if this operation is
        /// supported with the current kernel.
        pub const CODE: u8 = sys::IORING_OP_READV as _;
        #[inline]
        pub fn new(fd: impl sealed::UseFixed, iovec: *const libc::iovec, len: u32) -> Self {
            Readv {
                fd: fd.into(),
                iovec: iovec.into(),
                len: len.into(),
                ioprio: 0u16,
                offset: 0i64,
                rw_flags: 0i32,
            }
        }
        #[inline]
        pub const fn ioprio(mut self, ioprio: u16) -> Self {
            self.ioprio = ioprio;
            self
        }
        #[inline]
        pub const fn offset(mut self, offset: libc::off_t) -> Self {
            self.offset = offset;
            self
        }
        /// specified for read operations, contains a bitwise OR of per-I/O flags,
        /// as described in the `preadv2(2)` man page.
        #[inline]
        pub const fn rw_flags(mut self, rw_flags: types::RwFlags) -> Self {
            self.rw_flags = rw_flags;
            self
        }
        #[inline]
        pub fn build(self) -> Entry {
            let mut sqe = sqe_zeroed();
            self.prepare(&mut sqe);
            Entry(sqe)
        }
    }

    impl PrepareSQE for Readv {
        #[inline]
        fn prepare(&self, sqe: &mut sys::io_uring_sqe) {
            sqe.opcode = Readv::CODE;
            sqe.fd = self.fd.as_sqe_value() as _;
            sqe.__bindgen_anon_2.addr = self.iovec.as_sqe_value() as _;
            sqe.len = self.len.as_sqe_value() as _;
            sqe.ioprio = self.ioprio.as_sqe_value() as _;
            sqe.__bindgen_anon_1.off = self.offset.as_sqe_value() as _;
            sqe.__bindgen_anon_3.rw_flags = self.rw_flags.as_sqe_value() as _;
            {
                if match self.fd {
                    sealed::Target::Fixed(_) => true,
                    _ => false,
                } {
                    sqe.flags |= crate::squeue::Flags::FIXED_FILE.bits();
                };
            }
        }
    }

    /// Vectored read, equivalent to `preadv2(2)`.
    #[repr(transparent)]
    pub struct ReadvSqe {
        sqe: sys::io_uring_sqe,
    }

    impl ReadvSqe {
        /// The opcode of the operation. This can be passed to
        /// [`Probe::is_supported`](crate::Probe::is_supported) to check if this operation is
        /// supported with the current kernel.
        pub const CODE: u8 = sys::IORING_OP_READV as _;
        #[inline]
        pub fn prepare(&mut self, fd: sealed::Target, iovec: *const libc::iovec, len: u32) {
            self.sqe.opcode = Self::CODE;
            self.sqe.fd = fd.as_sqe_value() as _;
            self.sqe.__bindgen_anon_2.addr = iovec.as_sqe_value() as _;
            self.sqe.len = len.as_sqe_value() as _;
            self.sqe.ioprio = 0u16.as_sqe_value() as _;
            self.sqe.__bindgen_anon_1.off = 0i64.as_sqe_value() as _;
            self.sqe.__bindgen_anon_3.rw_flags = 0i32.as_sqe_value() as _;
            {
                if match fd {
                    sealed::Target::Fixed(_) => true,
                    _ => false,
                } {
                    self.sqe.flags |= crate::squeue::Flags::FIXED_FILE.bits();
                };
            }
        }
        #[inline]
        pub fn get_mut_sqe(&mut self) -> &mut sys::io_uring_sqe {
            &mut self.sqe
        }
        #[inline]
        pub fn ioprio(mut self, ioprio: u16) -> Self {
            self.sqe.ioprio = ioprio.as_sqe_value() as _;
            self
        }
        #[inline]
        pub fn offset(mut self, offset: libc::off_t) -> Self {
            self.sqe.__bindgen_anon_1.off = offset.as_sqe_value() as _;
            self
        }
        /// specified for read operations, contains a bitwise OR of per-I/O flags,
        /// as described in the `preadv2(2)` man page.
        #[inline]
        pub fn rw_flags(mut self, rw_flags: types::RwFlags) -> Self {
            self.sqe.__bindgen_anon_3.rw_flags = rw_flags.as_sqe_value() as _;
            self
        }
    }

    impl<'a> From<&'a mut sys::io_uring_sqe> for &'a mut ReadvSqe {
        #[inline]
        fn from(sqe: &'a mut sys::io_uring_sqe) -> &'a mut ReadvSqe {
            unsafe { mem::transmute(sqe) }
        }
    }

Introduce helper next_sqe() and move_forward() for SQ, it will be
reused in following patches.

Signed-off-by: Liu Jiang <gerry@linux.alibaba.com>
Introduce trait PrepareSQE to support prepare SQE in place by calling
opcode.prepare(&mut sqe).

Signed-off-by: Liu Jiang <gerry@linux.alibaba.com>
Add SubmissionQueue::push_command()/push_commands() to generate SQE
in place.

Signed-off-by: Liu Jiang <gerry@linux.alibaba.com>
Add unit test cases and benchmark for push_command().

Signed-off-by: Liu Jiang <gerry@linux.alibaba.com>
Currently the opcode module provides a data structure for each io-uring
operation code. And it works in following way to submit an SQE:
- generate an opcode object, such Nop, Readv etc.
- convert the opcode object into an SQE object
- copy the generated SQE object onto the next available SQE in the
  submission queue

One previous patch in this series implements PrepareSQE to optimize the
process to submit a request as:
- generate an opcode object, such Nop, Readv etc.
- convert the opcode object to an SQE in the submission queue in place

The process to submit a request could be further optimized as:
- get the next available SQE from the submission queue
- prepare the SQE for io uring operation
- commit the SQE

This patch enhance the opcode module to prepare SQE in place, without
involving the intermedia opcode structures. The way to achieve the goal
is to introduce a structure for each opcode to support in-place
preparatio.
    #[repr(transparent)]
    pub struct ReadvSqe {
        sqe: sys::io_uring_sqe,
    }

    impl ReadvSqe {
        #[inline]
        pub fn prepare(&mut self, fd: sealed::Target, iovec: *const libc::iovec, len: u32) {
	}
        #[inline]
        pub fn ioprio(mut self, ioprio: u16) -> Self {
            self.sqe.ioprio = ioprio.as_sqe_value() as _;
            self
        }
    }

    impl<'a> From<&'a mut sys::io_uring_sqe> for &'a mut ReadvSqe {
        #[inline]
        fn from(sqe: &'a mut sys::io_uring_sqe) -> &'a mut ReadvSqe {
            unsafe { mem::transmute(sqe) }
        }
    }

Signed-off-by: Liu Jiang <gerry@linux.alibaba.com>
Export SubmissionQueue::get_available_sqe() to prepare available SQE
for in-place preparation. And export SubmissionQueue::move_forward()
to commit prepared SQEs.

Sample code to use the new interface:
pub fn prepare_sqe(mut sq: SubmissionQueue<'_>) {
    unsafe {
        match sq.get_available_sqe(0) {
            Ok(sqe) => {
                let nop_sqe: &mut crate::opcode::NopSqe = sqe.into();
                nop_sqe.prepare();
                sq.move_forward(1);
            }
            Err(_) => return,
        }
    }
}

Signed-off-by: Liu Jiang <gerry@linux.alibaba.com>
Add benchmark for preparing SQE in place.

Signed-off-by: Liu Jiang <gerry@linux.alibaba.com>
@jiangliu
Copy link
Author

jiangliu commented Jan 3, 2022

I have also conduct a round of benchmark on one platform. For the Nop tests, the results are as below(in us):

version normal prepare prepare_sqe
original 13.782
13.834
13.815
first version 13.786 13.776
13.817 13.829
13.789 14.143
second version 13.779 13.818 13.966
13.704 13.754 13.939
13.734 13.825 13.931

For detailed analysis of the performance result, please refer to to #116

There are slight performance degradations for Nop, but I think the performance behave may change for actual workloads.

And the new interface enables new usages of the io uring crate:)

@quininer
Copy link
Member

quininer commented Jan 4, 2022

This is the same as I expected. opcode is just an entry builder by design. It should be constructed simply and pushed into sq immediately, so it should always be easy to optimize.

If this PR does not provide a visible performance improvement, I tend not to accept it.
Especially I don't like the API it introduces.

@jiangliu
Copy link
Author

jiangliu commented Jan 4, 2022

Thanks for your patience, let's close it because it doesn't bring much values:)

@jiangliu jiangliu closed this Jan 4, 2022
@quininer
Copy link
Member

quininer commented Jan 4, 2022

Anyway, thank you for trying, thank you. :D

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.

2 participants