highload: parse integers

- 5 mins read

Learning C++ always seemed extremely painful from the viewpoint who likes things easy: not just the language itself, but its kind of slightly insane build system. I like things easy; even Java to be honest seemed slightly less insane. But the industry I’m in quite likes it, mostly out of inertia. It isn’t entirely about C++ itself that I wanted to know more about, but a lot of the rabbitholes that it also invites upon the learning.

I’ve known about highload.fun for a while and decided that while it won’t teach me about software engineering in C++ in any meaningful way whatsoever, it’ll teach me a lot of fun tricks anyway, and who knows, they might be portable to other languages. What better way to begin your time at Recurse than learning extremely obscure trivia that you’ve always (?) been wanting to learn?

Warning: this will be a long and meandering post about a person doing this very blindly, e.g., vibecoding and investigating rabbit holes along the way. The goal is get to somewhere in the 4 or 5 digit ranks at least for the first puzzle; this I think might be achievable (?), but this exercise is the very definition of effing around and finding out (and in fact all of Recurse), so we’ll see!

Day 1

What better way than try the default code on default flags?

#include <iostream>

int main() {
    uint64_t sum = 0;
    while (std::cin) {
        uint64_t v = 0;
        std::cin >> v;
        sum += v;
    }

    std::cout << sum << std::endl;
    return 0;
}

flags: g++13.3.0 -O2 -std=c++17

Turns out this doesn’t even run; error messages (which I am told will lead you astray because the language is evidently prone to gaslighting you) suggest adding cstdint, so I dutifully add #include <cstdint> and there it goes.

Score: 1,715,461
Rank: 771 (out of 775)

I marvel that I’m not ranked dead last, and attribute it to variance. End of Day 1!

Day 2

Now that I’ve established a baseline, it’s time to move on. Poking around at the defaults, I see the -O2 flag used for the compiler, and wonder: is there a magic combo of flags to make this run a bit faster?

flag: -O3
Score: 1,715,519

Hm, that was worse, but yet again, probably variance at work and not a meaningful change. Ok, so what about -Ofast?

flag: -Ofast
Score: 1,715,402

Ok a bit better, but yet again, seems to be mostly variance at work. Clearly, fast math or letting the compiler do it for me wasn’t going anywhere (for now), so dropped this avenue. The next avenue to attack is input/output streams, maybe there are some optimizations there. Time to pull out the trusty claude chat and ask it how I could optimize cin/cout: It suggests a number of promising avenues to investigate, starting with synchronization:

# Disable iostream synchronization

ios_base::sync_with_stdio(false);
cin.tie(NULL);

Here, going back to the original default flags for compilation, is the final result I submit:

#include <iostream>
#include <cstdint>

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    uint64_t sum = 0;

    while (std::cin) {
           uint64_t v = 0;
        std::cin >> v;
        sum += v;
    }

    std::cout << sum << std::endl;
    return 0;
}

Evidently disabling this synchronization does wonders: I shot up ranks immediately with this change:

Score: 327,797
Rank: 544

Wow, two hundred ranks in one go. Ok, that was cool, but more importantly: why does this work? Cppreference tells me why: by default, being able to freely intermix c++ and c streams without going down the rabbit hole of buggy insanity introduces additional overhead to c++. But for this toy script, needing this particular guarantee is irrelevant, and getting rid of it gets rid of the overhead. Cool, makes sense.

What about the second part, std::cin.tie(NULL)? Actually, this is a second, separate optimization, does it make an impact if I remove it?

Score: 381,327
Rank: ~585

Mmm, yes, it does appear to be a useful optimization. So what does this one do? Evidently both the input and output buffers are, by default, tied to each other, which causes a forced flush on cout everytime there is a cin operation. But if I don’t need this flush at all (my toy only has one cout operation), removing all superfluous flushes to the stream add up. Neat! I didn’t really realize the many ways you can manipulate the streams in C++. But now I’m wondering: do I even really need a c++ stream then? What other overhead does C++ bring? What if I drop back down to using scanf?

#include <iostream>
#include <cstdint>

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    uint64_t sum = 0;
    uint64_t v = 0;

    while (scanf("%llu", &v) == 1) {
        sum += v;
    }

    std::cout << sum << std::endl;
    return 0;
}

Let’s go! And…. then… sadness:

Score: 429,700
Rank: ~597

I ask claude why and it suggests cin operations may be more aggressively optimizied, or fewer branches, and expect that this might be actually resolved with a visit to the compiler explorer and digging in, this exploration will be postponed for now to the next iteration.

At this point, I’m thinking my next step is seeing about cin buffers, and then, tentatively linux internals.

Day 3

TBD