How to create backoff that is faster than pure yield while preserving CPU efficiency?

I create backoff like this :

pub fn get_sender_auto(&self) -> &mut [MaybeUninit<T>] {
    if let Some(b) = self.get_sender() {
        return b;
    }

    loop {     
        let mut spin = 8;     

        for _ in 0..3 {
            for _ in 0..spin {
                if let Some(b) = self.get_sender() {
                    return b;
                }
                core::hint::spin_loop();
            }
            spin <<= 2;

            unsafe {
                libc::sched_yield();
            }   
        }

        println!("futex");

        self.producer.futex.store(FUTEX_WAITING);

        if let Some(b) = self.get_sender() {
            self.producer.futex.store(FUTEX_OPEN);
            return b;
        }

        self.producer.futex.wait(FUTEX_WAITING);

        if let Some(b) = self.get_sender() {
            return b;
        }     
    
    }
}

And then in the consumer, I wake the Futex in every call, aka :

// all processing to prepare the value

if self.producer.futex.load() == FUTEX_WAITING {
    if self.producer.futex.swap(FUTEX_OPEN) == FUTEX_WAITING {
        self.producer.futex.wake_one();
    }
}

// return the value

Now compared with the pure yield :

while sent < total_data {
    if let Some(s) = self.get_sender() {
        s.send(data);
        sent += total
    } else {
        std::thread::yield_now();
    }
}

The receiver is also pure yield :

loop {
    if let Some(val) = self.receive() {
        // using the value
    } else {
        std::thread::yield_now();
    }
}

The pure yield is always faster

── RUN 1 ──

── Thread Yield ──
source: 199999980000000, total: 199999980000000, match: true
Ring Bits        : 12
Ring Capacity    : 4096 elements
Total Producers  : 4
Total Messages   : 40000000 items
Execution Time   : 220.019619ms
Throughput       : 181.801.969 msg/sec

── Custom Backoff ──
source: 199999980000000, total: 199999980000000, match: true
Ring Bits        : 12
Ring Capacity    : 4096 elements
Total Producers  : 4
Total Messages   : 40000000 items
Execution Time   : 250.521769ms
Throughput       : 159.666.763 msg/sec

── RUN 2 ──

── Thread Yield ──
source: 199999980000000, total: 199999980000000, match: true
Ring Bits        : 12
Ring Capacity    : 4096 elements
Total Producers  : 4
Total Messages   : 40000000 items
Execution Time   : 199.973812ms
Throughput       : 200.026.191 msg/sec

── Custom Backoff ──
source: 199999980000000, total: 199999980000000, match: true
Ring Bits        : 12
Ring Capacity    : 4096 elements
Total Producers  : 4
Total Messages   : 40000000 items
Execution Time   : 253.88964ms
Throughput       : 157.548.768 msg/sec

── RUN 3 ──

── Thread Yield ──
source: 199999980000000, total: 199999980000000, match: true
Ring Bits        : 12
Ring Capacity    : 4096 elements
Total Producers  : 4
Total Messages   : 40000000 items
Execution Time   : 197.360402ms
Throughput       : 202.674.901 msg/sec

── Custom Backoff ──
source: 199999980000000, total: 199999980000000, match: true
Ring Bits        : 12
Ring Capacity    : 4096 elements
Total Producers  : 4
Total Messages   : 40000000 items
Execution Time   : 304.678084ms
Throughput       : 131.286.108 msg/sec

── RUN 4 ──

── Thread Yield ──
source: 199999980000000, total: 199999980000000, match: true
Ring Bits        : 12
Ring Capacity    : 4096 elements
Total Producers  : 4
Total Messages   : 40000000 items
Execution Time   : 215.068264ms
Throughput       : 185.987.459 msg/sec

── Custom Backoff ──
source: 199999980000000, total: 199999980000000, match: true
Ring Bits        : 12
Ring Capacity    : 4096 elements
Total Producers  : 4
Total Messages   : 40000000 items
Execution Time   : 317.148649ms
Throughput       : 126.123.822 msg/sec

── RUN 5 ──

── Thread Yield ──
source: 199999980000000, total: 199999980000000, match: true
Ring Bits        : 12
Ring Capacity    : 4096 elements
Total Producers  : 4
Total Messages   : 40000000 items
Execution Time   : 196.504987ms
Throughput       : 203.557.174 msg/sec

── Custom Backoff ──
source: 199999980000000, total: 199999980000000, match: true
Ring Bits        : 12
Ring Capacity    : 4096 elements
Total Producers  : 4
Total Messages   : 40000000 items
Execution Time   : 267.938161ms
Throughput       : 149.288.178 msg/sec

The pure yield is consistently has higher performance

The Futex in the producer is never be run because the println!("Futex") does not appear on console. So the problem is likely in the loop before Futex or the always call Futex wake up in the consumer. I'm not sure what is the exact cause, anyone know what is that causes the performance slower than pure yield?

Why is a spin loop started from 8 then exponential by 2 for 3 time with thread yield between the outer loop is slower than pure yield loop?

Does contantly call Futex wake up has overhead or it is no opt if the producer is not parked?

The machine used to test has 1 core CPU

Assuming the CPU doesn't have hyperthreads, what I quoted is exactly why -- generally for spin looping to actually be useful there has to be another CPU core that will do the atomic store to let the spin looping CPU stop spin looping, if you only have 1 core, the other threads will not be running while the spin looping thread is running, so the fastest way to make the other threads run is to tell the OS you don't want the currently running thread to hog all the CPU -- sched_yield or other blocking syscalls do that.

(I'm ignoring reasons you'd want a spin loop anyway in an OS kernel, since I'm assuming you aren't writing an OS kernel)

If you test using a 2 or more core CPU, then you'll likely find spin looping a bit of time before blocking is very useful, since that way you can do the sending/receiving stuff without needing to go through the OS assuming neither thread needs to wait for very long.

Yeah, I have the custom backoff has higher performance than the pure yield loop

It started when I did the development in my laptop that has 8 cores, previously there was no backoff or yield, just normal loop aka spin loop continously. After finished the code then I tested there alongside with STD MPSC Channel (I'm creating custom channel). The result was the custom channel is faster than STD Channel

Then I tested again in Ubuntu VPS because my laptop is Windows I run WSL2, the VPS has 1 core CPU that I though maybe the result can be different :

Model name:          Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz
BIOS Model name:     pc-i440fx-2.1  CPU @ 0.0GHz
BIOS CPU family:     1
CPU family:          6
Model:               85
Thread(s) per core:  1
Core(s) per socket:  1

The result was so supprising because all the custom channel suddently becames slower than STD Channel. Then I added yield, the custom channel back to be faster than STD channel again. But then I had question why Crossbeam uses backoff (as STD channel is Crossbeam) if yield is faster. So I want to test which one is faster, backoff or yield. Then I created custom yield, the result was it is also faster than STD channel (more because the custom channel). But I can't make it faster than pure yield yet. I forgot whether I have tried to run the post added yield in my laptop, I will check it first

But then does that mean there is no way to make custom backoff faster than pure yield in 1 core CPU?

generally yes, though nearly all computers (except embedded) have more than 1 core these days, even some of the cheaper VPSes have more than one core these days, e.g. Contabo's cheapest is 4 cores; even this $2/mo VPS has 2 cores (not that I'm endorsing them, idk if they're any good).

I test it in my laptop and Android, the result is same the backoff has lower performance :

My laptop CPU :

CPU(s):                   8
  On-line CPU(s) list:    0-7
Vendor ID:                GenuineIntel
  Model name:             Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
    CPU family:           6
    Model:                126
    Thread(s) per core:   2
    Core(s) per socket:   4

The result :

──RUN 1──
──Thread Yield──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 45.3894ms
  Throughput         : 881.263.026 msg/sec

──Custom Backoff──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 75.202999ms
  Throughput         : 531.893.681 msg/sec

──RUN 2──
──Thread Yield──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 48.898199ms
  Throughput         : 818.026.038 msg/sec

──Custom Backoff──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 60.4697ms
  Throughput         : 661.488.315 msg/sec

──RUN 3──
──Thread Yield──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 51.3029ms
  Throughput         : 779.683.019 msg/sec

──Custom Backoff──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 66.882601ms
  Throughput         : 598.062.865 msg/sec

──RUN 4──
──Thread Yield──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 45.1828ms
  Throughput         : 885.292.633 msg/sec

──Custom Backoff──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 58.423ms
  Throughput         : 684.661.862 msg/sec

──RUN 5──
──Thread Yield──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 36.6936ms
  Throughput         : 1.090.108.356 msg/sec

──Custom Backoff──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 46.7561ms
  Throughput         : 855.503.346 msg/sec

My Android CPU :

Architecture:             aarch64
  CPU op-mode(s):         32-bit, 64-bit
  Byte Order:             Little Endian
CPU(s):                   8
  On-line CPU(s) list:    0-7
Vendor ID:                ARM
  Model name:             Cortex-A55
    Model:                0
    Thread(s) per core:   1
    Core(s) per socket:   6
    Socket(s):            1
    Stepping:             r1p0
    CPU(s) scaling MHz:   84%
    CPU max MHz:          1800.0000
    CPU min MHz:          500.0000
    BogoMIPS:             26.00
    Flags:                fp asimd evtstrm aes pmu
                          ll sha1 sha2 crc32 atomi
                          cs fphp asimdhp cpuid as
                          imdrdm lrcpc dcpop asimd
                          dp
  Model name:             Cortex-A75
    Model:                1
    Thread(s) per core:   1
    Core(s) per socket:   2
    Socket(s):            1
    Stepping:             r3p1
    CPU(s) scaling MHz:   60%
    CPU max MHz:          2000.0000
    CPU min MHz:          850.0000
    BogoMIPS:             26.00
    Flags:                fp asimd evtstrm aes pmu
                          ll sha1 sha2 crc32 atomi
                          cs fphp asimdhp cpuid as
                          imdrdm lrcpc dcpop asimd
                          dp

The result :

──RUN 1──
──Thread Yield──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 165.272615ms
  Throughput         : 242.024.366 msg/sec

──Custom Backoff──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 182.590692ms
  Throughput         : 219.069.217 msg/sec

──RUN 2──
──Thread Yield──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 168.063ms
  Throughput         : 238.005.985 msg/sec

──Custom Backoff──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 186.583ms
  Throughput         : 214.381.803 msg/sec

──RUN 3──
──Thread Yield──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 187.425ms
  Throughput         : 213.418.700 msg/sec

──Custom Backoff──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 194.312385ms
  Throughput         : 205.854.094 msg/sec

──RUN 4──
──Thread Yield──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 176.684307ms
  Throughput         : 226.392.488 msg/sec

──Custom Backoff──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 182.465616ms
  Throughput         : 219.219.384 msg/sec

──RUN 5──
──Thread Yield──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 162.180923ms
  Throughput         : 246.638.132 msg/sec

──Custom Backoff──
source: 199999980000000, total: 199999980000000, match: true
  Ring Bits          : 12
  Ring Capacity      : 4096 elements
  Producer Count     : 4
  Total Messages     : 40000000 items
  Execution Time     : 183.851615ms
  Throughput         : 217.566.758 msg/sec

Because there results in multicore CPU are consistently the same, the custom backoff has lower performance than pure yield, what is possible the cause?

About the atomic you mentioned earlier, is what you mean is placing atomic flag within the spin loop then if the flag is true then stop/break the spin loop?

The println is never be run, it is just to check if the text Futex appears on the terminal then the Futex is run, but if not then the loop is always success before going to Futex

I just found this seems to be easy to use profiler for Rust because it automatically removes unrelated statistics to user space code like Libc, etc

I hope I can find something clearly with readable name not random name, because I tried to use profiler in the past with debug info enable still the result confused me because many non descriptive names :<

Sorry, I realized the moment after I posted it that I hadn't read it properly. :sweat_smile: Don't mind me.