/* Cost of passing an aggregate by value, measured. Companion to calling.html.
 *
 *   cc -O2 -o cost cost.c && ./cost
 *
 * Two attributes carry the whole method. `noinline` keeps the call; `noipa`
 * additionally stops the compiler reasoning about the body across the call
 * boundary. Without `noipa`, GCC infers that a by-value callee with no side
 * effects is pure, hoists it out of a loop whose argument never changes, and
 * the benchmark reports a difference of zero -- correctly, for a program that
 * no longer contains the copy. The loop mutates the struct for the same
 * reason: a loop-invariant argument is a copy the compiler may perform once.
 *
 * The reported delta is a dependency-chain latency, not a memory-bandwidth
 * figure: the caller's store into the outgoing parameter area is immediately
 * reloaded by the callee, so each call pays store-to-load forwarding rather
 * than streaming throughput.
 */

#include <stdio.h>
#include <time.h>

#define REPS  5000000L   /* calls per timed trial */
#define TRIALS      7    /* trials per case; the minimum is reported */

/* A bare call with no arguments at all, timed the same way as everything else.
   Absolute nanoseconds are a property of one machine; this control is the unit
   that lets a reader on a different machine compare shapes rather than
   numbers, and it is also the cheapest call the loop can possibly contain. */
__attribute__((noinline, noipa)) double nothing(void) { return 1.0; }

/* A volatile sink keeps each result live without creating a loop-carried
   floating-point dependency: an accumulator would add its own latency to both
   sides of the comparison, and a plain variable could be reduced to a single
   store after the loop. */
static volatile double sink;

static double now(void)
{
    struct timespec t;
    clock_gettime(CLOCK_MONOTONIC, &t);
    return t.tv_sec + t.tv_nsec * 1e-9;
}

/* One pair of callees and one timing loop per struct size. The callees touch a
   single field, so anything above the first eightbyte is pure passing cost.
   Each case is timed TRIALS times and the minimum is reported: this is a
   latency measurement on a shared machine, where every source of noise adds
   time and none subtracts it, so the minimum is the least contaminated
   estimate rather than a best case cherry-picked from a spread. */
#define PROBE(SZ)                                                             \
    typedef struct { double v[SZ / 8]; } S##SZ;                               \
    __attribute__((noinline, noipa))                                          \
    double by_value_##SZ(S##SZ s)         { return s.v[0]; }                  \
    __attribute__((noinline, noipa))                                          \
    double by_pointer_##SZ(const S##SZ *s) { return s->v[0]; }                \
                                                                              \
    static void run_##SZ(double unit)                                                \
    {                                                                         \
        S##SZ s;                                                              \
        for (int i = 0; i < SZ / 8; i++) s.v[i] = i;                          \
        double bv = 1e30, bp = 1e30;                                          \
                                                                              \
        for (int t = 0; t < TRIALS; t++) {                                    \
            double t0 = now();                                                \
            for (long i = 0; i < REPS; i++)                                   \
                { s.v[0] = (double)i; sink = by_value_##SZ(s); }              \
            double t1 = now();                                                \
            for (long i = 0; i < REPS; i++)                                   \
                { s.v[0] = (double)i; sink = by_pointer_##SZ(&s); }           \
            double t2 = now();                                                \
            double a = (t1 - t0) / REPS * 1e9, b = (t2 - t1) / REPS * 1e9;    \
            if (a < bv) bv = a;                                               \
            if (b < bp) bp = b;                                               \
        }                                                                     \
        printf("%6d %10.2f %10.2f %10.2f %9.2f %10.2f\n",                     \
               SZ, bv, bp, bv - bp, bv / bp, (bv - bp) / unit);               \
    }

PROBE(16) PROBE(32) PROBE(64) PROBE(128) PROBE(192)
PROBE(256) PROBE(384) PROBE(512) PROBE(768) PROBE(1024)

/* The control, measured exactly as the probes are so the comparison is fair. */
static double bare_call(void)
{
    double best = 1e30;
    for (int t = 0; t < TRIALS; t++) {
        double t0 = now();
        for (long i = 0; i < REPS; i++) sink = nothing();
        double t1 = now();
        double a = (t1 - t0) / REPS * 1e9;
        if (a < best) best = a;
    }
    return best;
}

int main(void)
{
    double unit = bare_call();
    printf("compiler: %s\n", __VERSION__);
    printf("bare call with no arguments: %.2f ns  (the unit below)\n\n", unit);
    printf("%6s %10s %10s %10s %9s %10s\n",
           "bytes", "by value", "by ptr", "delta", "ratio", "delta in");
    printf("%6s %10s %10s %10s %9s %10s\n",
           "", "ns/call", "ns/call", "ns/call", "", "bare calls");
    run_16(unit);  run_32(unit);  run_64(unit);   run_128(unit); run_192(unit);
    run_256(unit); run_384(unit); run_512(unit); run_768(unit); run_1024(unit);
    return 0;
}
