If you declare your function as #[inline(never)] you will get a function call instruction to prevent further optimizations.
The main limitation is that your function must not be empty after optimizations, so it must have some side effect (thanks to @hellow that suggests using compiler_fence instead of println!).
For example, this code (godbolt):
pub fn test_loop(num: i32) {
    for _i in 0..num {
        dummy();
    }
}
#[inline(never)]
pub extern fn dummy() {
    use std::sync::atomic::*;
    compiler_fence(Ordering::Release);
}
Will produce the following assembly (with -O), that I think you need:
example::test_loop:
        push    r14
        push    rbx
        push    rax
        test    edi, edi
        jle     .LBB0_3
        mov     ebx, edi
        mov     r14, qword ptr [rip + example::dummy@GOTPCREL]
.LBB0_2:
        call    r14
        add     ebx, -1
        jne     .LBB0_2
.LBB0_3:
        add     rsp, 8
        pop     rbx
        pop     r14
        ret
plus the code for dummy() that is actually empty:
example::dummy:
        ret