compute_ranks function
One possible optimization is to use local variables instead of directly writing to the out parameters. So instead of:

// compute averages
for (i = 0; i &lt; N; i++) {
    *avg += F[i];
    if (F[i] &gt;= 50.0) {
        *passing_avg += F[i];
        *num_passed += 1;
    }
}


you write:

int l_num_passed = 0;
float l_avg = 0.0;
float l_passing_avg = 0.0;

// compute averages
for (i = 0; i &lt; N; i++) {
    l_avg += F[i];
    if (F[i] &gt;= 50.0) {
        l_passing_avg += F[i];
        l_num_passed += 1;
    }
}

*num_passed = l_num_passed;
*avg = l_avg;
*passing_avg = l_passing_avg;


That way the compiler can use fast registers. If you directly write to the out parameter within the loop, the compiler cannot use registers as it must stay on the defensive side by assuming that avg could point to the same memory location as passing_avg or even num_passed.

This unfortunate feature of C and C++ is known as pointer aliasing and often prevents the compiler from making effective use of registers.

You can also use local variables for the other loop. Instead of:

// compute ranks
for (i = 0; i &lt; N; i++) {
    for (j = 0; j &lt; N; j++) {
        if (F[i] &lt; F[j]) {
            R[i] += 1;
        }
    }
}


write:

float fi;
int rank;

// compute ranks
for (i = 0; i &lt; N; i++) {
    fi = F[i];
    rank = 1;
    for (j = 0; j &lt; N; j++) {
        if (fi &lt; F[j]) {
            rank += 1;
        }
    }
    R[i] = rank;
}