[{"content":"Has it ever happened to you that on a Sunday evening you start being tortured by the question: \u0026ldquo;So just how many ways are there to print \u0026quot;Hello World\u0026quot; to the console in C++?\u0026rdquo;. I hope not, because that\u0026rsquo;s an occupational hazard at the very least. But I did ask myself exactly that, and I realized there isn\u0026rsquo;t a single source out there that answers it. So I decided to make such a source myself.\nSince C++ is now not just a programming language but also a metaprogramming language, we\u0026rsquo;ll look at two separate cases: printing \u0026quot;Hello World\u0026quot; at runtime and at compile time.\nMy goal was to count all the genuinely different ways. And that\u0026rsquo;s harder than it sounds. Because in the end almost everything boils down to something that calls write(2) through a varying number of abstraction layers, and the difference between methods is often microscopic. I used the following approach:\n1 A method is a distinct named entity that directly prints the string. All the code was tested on Fedora 44 (kernel 7.0, x86-64) with the GCC 16.1, Clang 22.1, glibc 2.43 stack. All the snippets live in the blog\u0026rsquo;s repository, along with scripts to run them.\nHere I\u0026rsquo;ve covered both methods that strictly conform to C++26 and methods that work specifically on Linux x86-64.\nRuntime First, code that fully conforms to C++26, then code that works only on POSIX systems, then POSIX extensions, and finally Linux-specific code.\nStandard C++ The canonical set These are the methods you\u0026rsquo;ll see in 99% of cases.\n#1. std::cout::operator\u0026lt;\u0026lt;\n1 2 3 4 5 6 #include \u0026lt;iostream\u0026gt; int main() { std::cout \u0026lt;\u0026lt; \u0026#34;Hello World\\n\u0026#34;; } Everyone knows that \\n on its own doesn\u0026rsquo;t flush the buffer. If you need a flush, you replace \\n with std::endl:\n1 std::cout \u0026lt;\u0026lt; \u0026#34;Hello World\u0026#34; \u0026lt;\u0026lt; std::endl; // \u0026#39;\\n\u0026#39; + flush or call an explicit flush():\n1 std::cout.flush(); For some reason most beginner tutorials use std::endl. Although, in my opinion, in most cases it isn\u0026rsquo;t needed, and it\u0026rsquo;s better to just use \\n.\n#2. printf()\n1 2 3 4 5 6 #include \u0026lt;cstdio\u0026gt; int main() { printf(\u0026#34;Hello World\\n\u0026#34;); } printf parses the format string looking for %, so technically there\u0026rsquo;s some wasted work here. Though compilers have long optimized this down to puts(\u0026quot;Hello World\u0026quot;).\nA digression on std::ios_base::sync_with_stdio(false)\nA short digression, familiar to anyone who\u0026rsquo;s done competitive programming. The \u0026ldquo;new\u0026rdquo; iostream-based API in C++ was added with C compatibility as a priority. In the sense that code which already used printf (or any other C stdio I/O) could start using std::cout without any extra setup and get a guaranteed, expected output order. This matters because C and C++ code is often linked together.\nSo, by default, the standard C++ streams are synchronized with the corresponding C streams: std::cout with stdout, std::cin with stdin, std::cerr and std::clog with stderr (plus their wide counterparts). When synchronization is on, the C++ streams can share a buffer with the corresponding FILE*. Two consecutive, different calls to operator\u0026lt;\u0026lt; and printf will run in the order they\u0026rsquo;re written. This, by the way, is in my opinion a place where C++ breaks the zero-overhead principle.\nYou can turn this behavior off by calling std::ios_base::sync_with_stdio(false). Important: the call has to come before any I/O operations, otherwise the behavior is implementation-defined. After that, the C++ streams get their own independent buffer. But if you then mix operator\u0026lt;\u0026lt; and printf, the output order is no longer guaranteed, because each mechanism buffers independently.\n#3. fprintf()\n1 2 3 4 5 6 #include \u0026lt;cstdio\u0026gt; int main() { fprintf(stdout, \u0026#34;Hello World\\n\u0026#34;); } By the way, the standard directly and explicitly defines printf as fprintf(stdout, ...).\n#4. puts()\n1 2 3 4 5 6 #include \u0026lt;cstdio\u0026gt; int main() { puts(\u0026#34;Hello World\u0026#34;); } In my view, this is the best method when you just need to print a string to the terminal without formatting. The function adds the \\n for you.\n#5. fputs()\n1 2 3 4 5 6 #include \u0026lt;cstdio\u0026gt; int main() { fputs(\u0026#34;Hello World\\n\u0026#34;, stdout); } Unlike puts(), this function does not add a \\n for you.\n#6. std::print() - C++23\n1 2 3 4 5 6 #include \u0026lt;print\u0026gt; int main() { std::print(\u0026#34;Hello World\\n\u0026#34;); } The function is built on top of std::format, which is type-safe. It also doesn\u0026rsquo;t drag in the whole of iostream. In my opinion, this is something that should have been in the language a very long time ago, not since 2023.\nA small detail: this std::print overload writes to FILE* stdout, not to std::cout. You can see here that the committee understands iostream wasn\u0026rsquo;t a great decision in hindsight.\n#7. std::println() - C++23\n1 2 3 4 5 6 #include \u0026lt;print\u0026gt; int main() { std::println(\u0026#34;Hello World\u0026#34;); } The same as std::print, but with a \\n automatically appended at the end.\n#8. std::print(stdout, …) - C++23\n1 2 3 4 5 6 7 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;print\u0026gt; int main() { std::print(stdout, \u0026#34;Hello World\\n\u0026#34;); } This overload takes any FILE*.\n#9. std::println(stdout, …) - C++23\n1 2 3 4 5 6 7 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;print\u0026gt; int main() { std::println(stdout, \u0026#34;Hello World\u0026#34;); } The same as #8, but with a \\n automatically appended at the end.\n#10. std::print(std::ostream\u0026amp;, …) - C++23\n1 2 3 4 5 6 7 #include \u0026lt;iostream\u0026gt; #include \u0026lt;print\u0026gt; int main() { std::print(std::cout, \u0026#34;Hello World\\n\u0026#34;); } And this overload takes a std::ostream\u0026amp;.\n#11. std::println(std::ostream\u0026amp;, …) - C++23\n1 2 3 4 5 6 7 #include \u0026lt;iostream\u0026gt; #include \u0026lt;print\u0026gt; int main() { std::println(std::cout, \u0026#34;Hello World\u0026#34;); } The same as #10, but with a \\n automatically appended at the end.\nOne level down: bytes and characters #12. std::ostream::write()\n1 2 3 4 5 6 7 8 #include \u0026lt;iostream\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { constexpr std::string_view msg = \u0026#34;Hello World\\n\u0026#34;; std::cout.write(msg.data(), msg.size()); } A direct write of n bytes. Works on any std::ostream, and likewise goes through the buffer.\n#13. std::streambuf::sputn()\n1 2 3 4 5 6 7 8 #include \u0026lt;iostream\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { constexpr std::string_view msg = \u0026#34;Hello World\\n\u0026#34;; std::cout.rdbuf()-\u0026gt;sputn(msg.data(), msg.size()); } What std::cout.write does under the hood. std::ostream::write first constructs a sentry (checks the stream\u0026rsquo;s state), then calls sputn on the std::streambuf.\n#14. std::ostream::put()\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;iostream\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { for (char c : std::string_view(\u0026#34;Hello World\\n\u0026#34;)) { std::cout.put(c); } } This puts one character at a time into the buffer. It goes to the kernel as a single write on flush.\n#15. std::streambuf::sputc()\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;iostream\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { for (char c : std::string_view(\u0026#34;Hello World\\n\u0026#34;)) { std::cout.rdbuf()-\u0026gt;sputc(c); } } The same as sputn, only character by character, and exactly how cout.put is implemented under the hood.\n#16. fwrite()\n1 2 3 4 5 6 7 8 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { constexpr std::string_view msg = \u0026#34;Hello World\\n\u0026#34;; fwrite(msg.data(), 1, msg.size(), stdout); } #17. putchar()\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { for (char c : std::string_view(\u0026#34;Hello World\\n\u0026#34;)) { putchar(c); } } #18. putc()\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { for (char c : std::string_view(\u0026#34;Hello World\\n\u0026#34;)) { putc(c, stdout); } } The same as putchar, but with an explicit FILE*. By the standard, putc may be a macro.\n#19. fputc()\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { for (char c : std::string_view(\u0026#34;Hello World\\n\u0026#34;)) { fputc(c, stdout); } } The same as putc, but guaranteed to be a function.\nFormatting as a separate operation: \u0026lt;format\u0026gt; I don\u0026rsquo;t count std::cout \u0026lt;\u0026lt; std::format(\u0026quot;…\u0026quot;), because it\u0026rsquo;s essentially just operator\u0026lt;\u0026lt;.\n#20. std::format_to()\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;format\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;iterator\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { constexpr std::string_view message = \u0026#34;Hello World\u0026#34;; std::format_to(std::ostream_iterator\u0026lt;char\u0026gt;(std::cout), \u0026#34;{}\\n\u0026#34;, message); } Formats straight into an output iterator.\n#21. std::format_to_n()\n1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;format\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;iterator\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { constexpr std::string_view message = \u0026#34;Hello World\u0026#34;; // +1 accounts for the \u0026#39;\\n\u0026#39; appended by the format string constexpr auto cap = message.size() + 1; std::format_to_n(std::ostream_iterator\u0026lt;char\u0026gt;(std::cout), cap, \u0026#34;{}\\n\u0026#34;, message); } The same as format_to, but with a limit on the number of characters. Used when the message is formatted into a fixed-size buffer.\niostream + STL algorithms Why write a loop when you can glue together three templates?\n#22. STL algorithm + output iterator\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;algorithm\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;iterator\u0026gt; #include \u0026lt;string_view\u0026gt; int main() { constexpr std::string_view msg = \u0026#34;Hello World\\n\u0026#34;; std::ranges::copy(msg, std::ostream_iterator\u0026lt;char\u0026gt;(std::cout)); } Instead of std::ranges::copy, std::copy, std::for_each, and std::ranges::for_each will work just as well here.\nAnd here\u0026rsquo;s an important detail: which output iterator you pick determines what the bytes pass through. std::ostream_iterator\u0026lt;char\u0026gt; sends each character through operator\u0026lt;\u0026lt; (that is, formatted output, like in #1). Whereas std::ostreambuf_iterator\u0026lt;char\u0026gt; writes straight into the streambuf via sputc, bypassing the entire formatting layer (like sputc, #15):\n1 std::ranges::copy(msg, std::ostreambuf_iterator\u0026lt;char\u0026gt;(std::cout)); What if several threads write to cout at once? #23. std::osyncstream - C++20\n1 2 3 4 5 6 7 #include \u0026lt;iostream\u0026gt; #include \u0026lt;syncstream\u0026gt; int main() { std::osyncstream(std::cout) \u0026lt;\u0026lt; \u0026#34;Hello World\\n\u0026#34;; } osyncstream accumulates output in its own buffer and atomically transfers it to the target stream on destruction. This exists because if you have several threads writing to an ostream without synchronization, their lines can get interleaved.\nA printf() call is atomic (stdio takes the FILE* lock for the duration of the call), and in practice std::cout::operator\u0026lt;\u0026lt; is too, because by default libstdc++ goes through the same lock. That said, the standard doesn\u0026rsquo;t guarantee atomicity of a single \u0026lt;\u0026lt;, only the absence of a data race. But std::cout \u0026lt;\u0026lt; \u0026quot;Hello\u0026quot; \u0026lt;\u0026lt; \u0026quot;World\u0026quot; is already 2 separate operator calls, and a std::cout::operator\u0026lt;\u0026lt; running in another thread can wedge itself in between them.\nstd::osyncstream fuses the entire operator\u0026lt;\u0026lt; sequence into a single atomic flush. Essentially it\u0026rsquo;s the same as assembling the string in a std::ostringstream and then doing std::cout \u0026lt;\u0026lt; stream.str() once.\nstderr and the wide streams #24. std::cerr::operator\u0026lt;\u0026lt;\n1 2 3 4 5 6 #include \u0026lt;iostream\u0026gt; int main() { std::cerr \u0026lt;\u0026lt; \u0026#34;Hello World\\n\u0026#34;; } The same operator\u0026lt;\u0026lt;, but a different stream. std::cerr is the standard error stream (descriptor 2 on Linux). It exists for convenience, so you can easily separate the error stream from regular output via redirection.\ncerr has the unitbuf flag set, so it flushes after every output operation. Because we usually want to learn about an error the moment it happens, not whenever the buffer decides to flush.\n#25. std::clog::operator\u0026lt;\u0026lt; - stderr, but buffered\n1 2 3 4 5 6 #include \u0026lt;iostream\u0026gt; int main() { std::clog \u0026lt;\u0026lt; \u0026#34;Hello World\\n\u0026#34;; } The same as cerr, but without the unitbuf flag set. It\u0026rsquo;s intended for diagnostic messages that aren\u0026rsquo;t \u0026ldquo;urgent\u0026rdquo;.\nI could count each cout method listed above 2 more times here (once for cerr, once for clog), but I won\u0026rsquo;t.\n#26. fprintf(stderr, …) - C stdio to stderr\n1 2 3 4 5 6 #include \u0026lt;cstdio\u0026gt; int main() { fprintf(stderr, \u0026#34;Hello World\\n\u0026#34;); } The same functionality as cerr, but in cstdio.\n#27-29. Wide streams: std::wcout, std::wcerr, std::wclog\n1 2 3 4 5 6 #include \u0026lt;iostream\u0026gt; int main() { std::wcout \u0026lt;\u0026lt; L\u0026#34;Hello World\\n\u0026#34;; } This trio mirrors cout/cerr/clog: wcout writes to stdout, wcerr/wclog to stderr. The difference is that the wide streams take wchar_t and convert it to char through the locale\u0026rsquo;s codecvt, something like use_facet\u0026lt;codecvt\u0026lt;...\u0026gt;\u0026gt;(getloc()). For ASCII the conversion is trivial. The output is the same write(1, \u0026quot;Hello World\\n\u0026quot;, 12).\nLocales in general are one of C++\u0026rsquo;s problem areas, and among other things they backfired especially painfully in regular expressions. There\u0026rsquo;s a meme that for some regexes it\u0026rsquo;s faster to spin up a Python script than to wait for std::regex to finish. It\u0026rsquo;s yet another example of the zero-overhead principle being violated.\n#30-33. Wide streams: the lower entry points (wcout.write etc.)\nEverything cout has (#12-#15), wcout has too, just templated on wchar_t. The next four methods correspond to #12-#15: wcout.write (#30), wcout.rdbuf()-\u0026gt;sputn (#31), wcout.put (#32), wcout.rdbuf()-\u0026gt;sputc (#33). I could also count wcerr/wclog and their methods separately here, but I won\u0026rsquo;t.\n#34-39. Wide C stdio: wprintf and company\n1 2 3 4 5 6 #include \u0026lt;cwchar\u0026gt; int main() { wprintf(L\u0026#34;Hello World\\n\u0026#34;); } Just like iostream, cstdio has its own six wide functions (or rather, the other way around): wprintf (#34), fwprintf (#35), fputws (#36), putwchar (#37), putwc (#38), fputwc (#39). They all convert wchar_t through the locale\u0026rsquo;s wcrtomb. For ASCII the output is again write(1, …, 12).\nLet another process print it (standard C) #40. system()\n1 2 3 4 5 6 #include \u0026lt;cstdlib\u0026gt; int main() { return system(\u0026#34;echo Hello World\u0026#34;); } This launches echo, which prints \u0026ldquo;Hello World\u0026rdquo;. You shouldn\u0026rsquo;t do this, because it\u0026rsquo;s slow and dangerous due to shell injection.\noutput as a side effect of diagnostics Here \u0026ldquo;Hello World\u0026rdquo; ends up in the terminal not because we print it, but because a library or the runtime reports something with it to stderr.\n#41. An uncaught throw\n1 2 3 4 5 6 #include \u0026lt;stdexcept\u0026gt; int main() { throw std::runtime_error(\u0026#34;Hello World\u0026#34;); } No one catches the exception -\u0026gt; std::terminate is called -\u0026gt; the runtime prints what() to stderr and kills the process:\n1 2 terminate called after throwing an instance of \u0026#39;std::runtime_error\u0026#39; what(): Hello World This is a hack already, but technically our string ended up in the terminal. The exact text of this message is implementation-defined. It\u0026rsquo;s produced by the standard library.\n#42. assert()\n1 2 3 4 5 6 #include \u0026lt;cassert\u0026gt; int main() { assert(false \u0026amp;\u0026amp; \u0026#34;Hello World\u0026#34;); } 1 a.out: hello.cpp:5: int main(): Assertion `false \u0026amp;\u0026amp; \u0026#34;Hello World\u0026#34;\u0026#39; failed. Works only as long as NDEBUG isn\u0026rsquo;t defined. Otherwise assert expands to nothing and Hello World disappears.\n#43-45. contract_assert, pre, post - C++26 Contracts\n1 2 3 4 5 // build with -fcontracts int main() { contract_assert(false \u0026amp;\u0026amp; \u0026#34;Hello World\u0026#34;); } Three new constructs from Contracts in C++26. contract_assert is the evolution of assert.\n1 2 int f(int x) pre(false \u0026amp;\u0026amp; \u0026#34;Hello World\u0026#34;) { return x; } // #44 int g(int x) post(false \u0026amp;\u0026amp; \u0026#34;Hello World\u0026#34;) { return x; } // #45 All three go through the contract-violation handler rather than through abort, and its behavior can be switched with the compiler flag -fcontract-evaluation-semantic=[ignore|observe|enforce|quick_enforce]. The system as a whole is very flexible and deserves its own topic, of which there are plenty right now. GCC 16 (with -fcontracts) under the default enforce prints to stderr:\n1 2 3 contract violation in function int main() at hello.cpp:4: false \u0026amp;\u0026amp; \u0026#34;Hello World\u0026#34; [assertion_kind: assert, semantic: enforce, mode: predicate_false, terminating: yes] terminate called without an active exception The key difference between the three is the assertion_kind field: assert, pre, or post. For now only GCC can do Contracts. The stable version of Clang doesn\u0026rsquo;t have them yet.\n#46. perror()\n1 2 3 4 5 6 7 8 #include \u0026lt;cerrno\u0026gt; #include \u0026lt;cstdio\u0026gt; int main() { errno = 0; perror(\u0026#34;Hello World\u0026#34;); } perror prints to stderr the string, a colon, and a description of the current errno. Since errno is zeroed out in the example, the description will be \u0026ldquo;Success\u0026rdquo;:\n1 Hello World: Success A hack? A hack. But what are you gonna do about it)\nSubtotal so far: 46 methods. And that\u0026rsquo;s all standard C++26.\nBonus I\u0026rsquo;d also like to look at methods that aren\u0026rsquo;t part of the standard but can still be used.\nPOSIX Code that conforms to the POSIX standard and works on all systems that implement it.\nDirect I/O bypassing the buffers #47. write()\n1 2 3 4 5 6 7 8 #include \u0026lt;string_view\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { constexpr std::string_view msg = \u0026#34;Hello World\\n\u0026#34;; write(STDOUT_FILENO, msg.data(), msg.size()); } This is the very write(2) that almost everything else in this article boils down to.\n#48. writev()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include \u0026lt;iterator\u0026gt; #include \u0026lt;sys/uio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { char hello[] = \u0026#34;Hello \u0026#34;; char world[] = \u0026#34;World\\n\u0026#34;; iovec iov[] = { { hello, sizeof(hello) - 1 }, { world, sizeof(world) - 1 }, }; writev(STDOUT_FILENO, iov, std::size(iov)); } Gathers data from several separate buffers into a single system call.\n#49. dprintf()\n1 2 3 4 5 6 7 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { dprintf(STDOUT_FILENO, \u0026#34;Hello World\\n\u0026#34;); } printf for file descriptors. Formats like printf, but writes straight to an fd, without a FILE*.\nAnd now an honorable mention that does not count. There\u0026rsquo;s also pwrite() - a positioned write at an offset:\n1 pwrite(STDOUT_FILENO, \u0026#34;Hello World\\n\u0026#34;, 12, 0); The problem is that pwrite requires a seekable descriptor. If you write to a file (./a.out \u0026gt; out.txt), it works. But if you write to a terminal or a pipe, you get ESPIPE (Illegal seek), and nothing gets printed, so this method doesn\u0026rsquo;t count.\nThrough the filesystem You can also reach a descriptor through the filesystem, simply by opening the right path.\n#50. open(\u0026quot;/dev/tty\u0026quot;) + write()\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;string_view\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { constexpr std::string_view msg = \u0026#34;Hello World\\n\u0026#34;; int fd = open(\u0026#34;/dev/tty\u0026#34;, O_WRONLY); write(fd, msg.data(), msg.size()); close(fd); } /dev/tty is the process\u0026rsquo;s controlling terminal, regardless of where stdout is redirected. Run ./a.out \u0026gt; /dev/null and you\u0026rsquo;ll still see \u0026ldquo;Hello World\u0026rdquo; in the terminal, because it writes past the redirection. This method requires a controlling terminal to be present. When tested in a headless environment with no tty, open returns -1, so I tested it under a real pseudo-terminal.\nA related curiosity that doesn\u0026rsquo;t make the list, because it no longer works: ioctl(fd, TIOCSTI, \u0026amp;c) adds a character not to the terminal\u0026rsquo;s output but to its input queue. Modern kernels disable TIOCSTI by default (CONFIG_LEGACY_TIOCSTI) and require CAP_SYS_ADMIN.\nLet another process print it (POSIX) #51. execlp()\n1 2 3 4 5 6 #include \u0026lt;unistd.h\u0026gt; int main() { execlp(\u0026#34;echo\u0026#34;, \u0026#34;echo\u0026#34;, \u0026#34;Hello World\u0026#34;, static_cast\u0026lt;char*\u0026gt;(nullptr)); } This replaces the process with echo. After a successful exec, \u0026ldquo;our\u0026rdquo; code literally no longer exists.\n#52. fork() + execvp()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include \u0026lt;sys/wait.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { if (fork() == 0) { char arg0[] = \u0026#34;echo\u0026#34;; char arg1[] = \u0026#34;Hello World\u0026#34;; char* args[] = { arg0, arg1, nullptr }; execvp(\u0026#34;echo\u0026#34;, args); _exit(127); // only reached if exec failed } wait(nullptr); } The classic Unix pattern, and the fundamental difference from the previous one: we fork, the child becomes echo, and the parent stays alive and waits for the child to finish. Note the _exit(127) (not exit()) after exec. If exec happens to fail, the child must not fall through into the parent\u0026rsquo;s logic.\nYou might ask me: why are there only two exec methods and not six? The exec* family (execl, execlp, execle, execv, execvp, execve) differs only in how arguments are passed. They all boil down to a single execve system call. So I decided not to push my luck here and count the exec variants, and instead count only the process-handling pattern: replace yourself (#51) or fork and outlive the child (#52).\n#53. posix_spawn()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include \u0026lt;spawn.h\u0026gt; #include \u0026lt;sys/wait.h\u0026gt; extern char** environ; int main() { char arg0[] = \u0026#34;/bin/echo\u0026#34;; char arg1[] = \u0026#34;Hello World\u0026#34;; char* args[] = { arg0, arg1, nullptr }; pid_t pid{}; posix_spawn(\u0026amp;pid, \u0026#34;/bin/echo\u0026#34;, nullptr, nullptr, args, environ); waitpid(pid, nullptr, 0); } A standardized alternative to the fork + exec combo in a single call. On top of that, in the case where fork is expensive, posix_spawn can be more efficient, because it\u0026rsquo;s implemented through lighter primitives (on Linux - through clone/vfork).\nWhy can fork be expensive? Intuitively fork is nearly free, because it doesn\u0026rsquo;t copy physical memory - it works through copy-on-write (COW). At the same time, its cost depends on the size of the page tables: the kernel has to duplicate all the parent\u0026rsquo;s PTEs, mark every writable page as read-only for COW, and do a TLB shootdown across all the cores the process ran on. For a process with a large address space, that\u0026rsquo;s already noticeable. posix_spawn via vfork/clone(CLONE_VM|CLONE_VFORK) avoids all of this: the child borrows the parent\u0026rsquo;s address space, so there\u0026rsquo;s no need to duplicate the page tables.\nAsynchronous input-output #54. aio_write() - POSIX AIO\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // link with -lrt #include \u0026lt;aio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { static char msg[] = \u0026#34;Hello World\\n\u0026#34;; aiocb cb{}; cb.aio_fildes = STDOUT_FILENO; cb.aio_buf = msg; cb.aio_nbytes = sizeof(msg) - 1; aio_write(\u0026amp;cb); const aiocb* list[] = { \u0026amp;cb }; aio_suspend(list, 1, nullptr); // block until the write completes } Asynchronous input-output. This queues a write and waits for completion via aio_suspend. On glibc, POSIX AIO is implemented with a pool of helper threads that do ordinary synchronous I/O: on a non-seekable descriptor (terminal, pipe) the thread tries to do a pwrite, which leads to ESPIPE, and then falls back to the very same write(1, \u0026quot;Hello World\\n\u0026quot;, 12).\n#55. lio_listio() - batched POSIX AIO\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // link with -lrt #include \u0026lt;aio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { static char msg[] = \u0026#34;Hello World\\n\u0026#34;; aiocb cb{}; cb.aio_fildes = STDOUT_FILENO; cb.aio_buf = msg; cb.aio_nbytes = sizeof(msg) - 1; cb.aio_lio_opcode = LIO_WRITE; aiocb* list[] = { \u0026amp;cb }; lio_listio(LIO_WAIT, list, 1, nullptr); } The same as the previous method, but instead of a single operation this one takes a whole list of aiocb and submits it in a single call. LIO_WAIT additionally makes this thread block until the entire list is done.\nAnd one more honorable mention that doesn\u0026rsquo;t count: send().\n1 send(STDOUT_FILENO, \u0026#34;Hello World\\n\u0026#34;, 12, 0); send is write for sockets. If your stdout is a socket (for example, the program was launched under inetd or socat), it works. I checked by substituting a socket on descriptor 1, and it worked. But in an ordinary terminal it leads to ENOTSOCK. So I don\u0026rsquo;t count it as a separate method.\n_unlocked - the same functions without the internal lock By default, every stdio call locks the FILE* for thread safety. The _unlocked family doesn\u0026rsquo;t lock - that\u0026rsquo;s the whole difference. It\u0026rsquo;s faster, but you have to guarantee that no one else is writing to that stream at the same time.\nputc_unlocked/putchar_unlocked are part of POSIX. The rest (in particular all the wide ones) are glibc extensions, but I\u0026rsquo;ll list them all here because, again, what are you gonna do about it.\n#56-60. Narrow: putchar_unlocked (#56), putc_unlocked (#57), fputc_unlocked (#58), fputs_unlocked (#59), fwrite_unlocked (#60) - twins of #17/#18/#19/#5/#16 without the lock.\n1 2 3 4 5 6 #include \u0026lt;cstdio\u0026gt; int main() { fputs_unlocked(\u0026#34;Hello World\\n\u0026#34;, stdout); } #61-64. Wide (glibc): fputws_unlocked (#61), putwchar_unlocked (#62), putwc_unlocked (#63), fputwc_unlocked (#64) - twins of #36/#37/#38/#39 without the lock.\n1 2 3 4 5 6 #include \u0026lt;cwchar\u0026gt; int main() { fputws_unlocked(L\u0026#34;Hello World\\n\u0026#34;, stdout); } Subtotal so far: 64 methods.\nExtensions POSIX isn\u0026rsquo;t the whole Unix ecosystem. There\u0026rsquo;s a pile of extensions that aren\u0026rsquo;t in any standard.\n\u0026lt;err.h\u0026gt; (BSD) and \u0026lt;error.h\u0026gt; (GNU) The BSD \u0026lt;err.h\u0026gt; family gives four such functions, and the GNU \u0026lt;error.h\u0026gt; extension gives two more.\n#65-68. err(), warn(), errx(), warnx() - BSD \u0026lt;err.h\u0026gt;\n1 2 3 4 5 6 #include \u0026lt;err.h\u0026gt; int main() { warnx(\u0026#34;Hello World\u0026#34;); // \u0026#34;\u0026lt;progname\u0026gt;: Hello World\u0026#34; to stderr } The four differ in two respects: whether to append : strerror(errno) (like perror) and whether to exit the program. warn/err append strerror, warnx/errx don\u0026rsquo;t. err/errx call exit() at the end, warn/warnx don\u0026rsquo;t.\n#69-70. error(), error_at_line() - GNU \u0026lt;error.h\u0026gt;\n1 2 3 4 5 6 #include \u0026lt;error.h\u0026gt; int main() { error(0, 0, \u0026#34;Hello World\u0026#34;); // \u0026#34;\u0026lt;progname\u0026gt;: Hello World\u0026#34; to stderr } error(status, errnum, …) appends strerror when errnum != 0, and exits when status != 0. error_at_line does the same, plus it adds a file:line: prefix.\nSubtotal so far: 70 methods.\nLinux-only Through procfs stdout is a file, and you can open it by its path in the filesystem. On Linux, /dev/stdout is a symlink to /proc/self/fd/1. POSIX itself doesn\u0026rsquo;t standardize this path. On BSD, for example, /dev/stdout also exists, but through a different mechanism.\n#71. std::ofstream(\u0026quot;/dev/stdout\u0026quot;)\n1 2 3 4 5 6 #include \u0026lt;fstream\u0026gt; int main() { std::ofstream(\u0026#34;/dev/stdout\u0026#34;) \u0026lt;\u0026lt; \u0026#34;Hello World\\n\u0026#34;; } You can do the same thing in C via fopen(\u0026quot;/dev/stdout\u0026quot;, \u0026quot;w\u0026quot;) + fprintf, or through other paths to the same descriptor: /dev/fd/1 or /proc/self/fd/1. I count this as 1 method, \u0026ldquo;open an fd through the filesystem\u0026rdquo;.\nsyscall #72. syscall(SYS_write, …)\n1 2 3 4 5 6 7 8 9 #include \u0026lt;string_view\u0026gt; #include \u0026lt;sys/syscall.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { constexpr std::string_view msg = \u0026#34;Hello World\\n\u0026#34;; syscall(SYS_write, 1, msg.data(), msg.size()); } This bypasses even the libc write() wrapper and invokes the system call by its number.\n#73. Inline assembly - x86-64\n1 2 3 4 5 6 7 8 9 10 11 12 13 int main() { const char msg[] = \u0026#34;Hello World\\n\u0026#34;; asm volatile( \u0026#34;mov $1, %%rax\\n\u0026#34; // syscall number: write \u0026#34;mov $1, %%rdi\\n\u0026#34; // fd: stdout \u0026#34;mov %0, %%rsi\\n\u0026#34; // buf: msg \u0026#34;mov %1, %%rdx\\n\u0026#34; // count: msg length, without the \u0026#39;\\0\u0026#39; \u0026#34;syscall\u0026#34; : : \u0026#34;r\u0026#34;(msg), \u0026#34;i\u0026#34;(sizeof(msg) - 1) : \u0026#34;rax\u0026#34;, \u0026#34;rdi\u0026#34;, \u0026#34;rsi\u0026#34;, \u0026#34;rdx\u0026#34;, \u0026#34;rcx\u0026#34;, \u0026#34;r11\u0026#34;, \u0026#34;memory\u0026#34;); } The lowest level available from C++: the syscall instruction itself. rcx and r11 in the clobber list aren\u0026rsquo;t there by accident: the syscall instruction clobbers them, storing RIP and RFLAGS in them respectively. memory in the clobbers tells the compiler not to keep memory values in registers across the asm boundary.\nMoving data with the kernel\u0026rsquo;s help The next three methods are interesting in that the data moves to stdout inside the kernel, barely touching our userspace, and the final output is done not by write but by their own system call.\n#74. sendfile() with memfd_create()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include \u0026lt;string_view\u0026gt; #include \u0026lt;sys/mman.h\u0026gt; #include \u0026lt;sys/sendfile.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { constexpr std::string_view msg = \u0026#34;Hello World\\n\u0026#34;; constexpr auto len = msg.size(); int fd = memfd_create(\u0026#34;hello\u0026#34;, 0); // \u0026#34;hello\u0026#34; is just a debug label, not output write(fd, msg.data(), len); lseek(fd, 0, SEEK_SET); sendfile(STDOUT_FILENO, fd, nullptr, len); close(fd); } memfd_create creates an anonymous file named \u0026ldquo;hello\u0026rdquo; that lives in RAM and is visible in /proc/self/fd. write fills this file, and then sendfile copies the data from it to stdout in kernel-space. For extra fun, you can fill this memfd not via write but via mmap + memcpy.\n#75. splice() through a pipe\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;string_view\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { constexpr std::string_view msg = \u0026#34;Hello World\\n\u0026#34;; constexpr auto len = msg.size(); int pfd[2]{}; pipe(pfd); write(pfd[1], msg.data(), len); splice(pfd[0], nullptr, STDOUT_FILENO, nullptr, len, 0); close(pfd[0]); close(pfd[1]); } splice moves data between descriptors through the kernel, without copying into userspace. One of the descriptors must be a pipe. The output to stdout here is done by the splice system call itself. There are similar methods like vmsplice (maps userspace pages into a pipe) and tee (duplicates data between two pipes).\nThere\u0026rsquo;s also copy_file_range, which likewise copies data between two descriptors, but both descriptors must be regular files. This method can\u0026rsquo;t copy to a terminal or a pipe.\n#76. io_uring\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // link with -luring #include \u0026lt;liburing.h\u0026gt; #include \u0026lt;string_view\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { constexpr std::string_view msg = \u0026#34;Hello World\\n\u0026#34;; io_uring ring{}; io_uring_queue_init(1, \u0026amp;ring, 0); io_uring_sqe* sqe = io_uring_get_sqe(\u0026amp;ring); io_uring_prep_write(sqe, STDOUT_FILENO, msg.data(), msg.size(), 0); io_uring_submit(\u0026amp;ring); io_uring_cqe* cqe = nullptr; io_uring_wait_cqe(\u0026amp;ring, \u0026amp;cqe); io_uring_cqe_seen(\u0026amp;ring, cqe); io_uring_queue_exit(\u0026amp;ring); } The most modern Linux I/O API. A submission queue, a completion queue, a buffer ring, all shared between the kernel and userspace - the whole thing was devised to do a large number of I/O operations as efficiently as possible. For \u0026ldquo;Hello World\u0026rdquo;, as we can see, it works too. There\u0026rsquo;s no synchronous write here at all. The kernel performs the I/O from our SQE, and we just submit and wait for completion. This is clearly visible in strace: there\u0026rsquo;s not a single write(1, …) there, only io_uring_setup and io_uring_enter, inside which the kernel does the write itself:\n1 2 3 $ strace -e io_uring_setup,io_uring_enter,write ./io-uring io_uring_setup(1, {...}) = 3 io_uring_enter(3, 1, 0, 0, NULL, 8) = 1 # SQE submitted; the write happens in-kernel Runtime total: 76 methods.\nCompile-time - the program never even runs In this section we\u0026rsquo;ll look at how to make \u0026ldquo;Hello World\u0026rdquo; appear during compilation rather than execution.\nStandard C++ #77. static_assert - C++11\n1 static_assert(false, \u0026#34;Hello World\u0026#34;); 1 error: static assertion failed: Hello World The most direct way to make the compiler print what you want. You can also defer this to template instantiation through a value-dependent expression:\n1 2 3 4 5 6 7 template \u0026lt;int N\u0026gt; struct HelloWorld { static_assert(N != N, \u0026#34;Hello World\u0026#34;); }; template struct HelloWorld\u0026lt;42\u0026gt;; N != N depends on the template parameter, so the check is deferred until instantiation. Thanks to CWG2518, modern GCC/Clang no longer fail even on a non-dependent static_assert(false).\n#78. [[deprecated]] - C++14\n1 2 3 4 [[deprecated(\u0026#34;Hello World\u0026#34;)]] void f() {} int main() { f(); } Compilation succeeds, but with a warning:\n1 warning: \u0026#39;void f()\u0026#39; is deprecated: Hello World [-Wdeprecated-declarations] #79. [[nodiscard(\u0026quot;…\u0026quot;)]] - C++20\n1 2 3 4 [[nodiscard(\u0026#34;Hello World\u0026#34;)]] int f() { return 0; } int main() { f(); } Compilation succeeds, but if you ignore the return value (which is exactly what we do), you get a warning:\n1 warning: ignoring return value of \u0026#39;int f()\u0026#39;, declared with attribute \u0026#39;nodiscard\u0026#39;: \u0026#39;Hello World\u0026#39; [-Wunused-result] The ability to add a reason to [[nodiscard]] was added in C++20; [[nodiscard]] itself in C++17.\n#80. = delete(\u0026quot;…\u0026quot;) - C++26\n1 2 3 void f() = delete(\u0026#34;Hello World\u0026#34;); int main() { f(); } 1 error: use of deleted function \u0026#39;void f()\u0026#39;: Hello World The ability to specify a reason for deleting a function was adopted only in C++26; GCC 16 already supports it.\n#81. throw at compile time - C++26\nIn C++26 you can throw exceptions at compile time already, and if an exception escapes a constexpr expression, the compiler is required to diagnose it. GCC 16 then prints what() right into the error text:\n1 2 3 4 5 #include \u0026lt;stdexcept\u0026gt; constexpr int hello() { throw std::runtime_error(\u0026#34;Hello World\u0026#34;); } constexpr int x = hello(); // forces constant evaluation -\u0026gt; the throw escapes 1 error: uncaught exception of type \u0026#39;std::runtime_error\u0026#39;; \u0026#39;what()\u0026#39;: \u0026#39;Hello World\u0026#39; In fact, compilers can already execute a large portion of C++ code at compile time. A small caveat: for now only GCC can do this. Clang 22 hasn\u0026rsquo;t yet implemented throwing exceptions in constant evaluation (P3068). It just rejects the throw as a non-constant expression, never reaching what().\n#82. #warning - C++23\n1 #warning \u0026#34;Hello World\u0026#34; 1 warning: #warning \u0026#34;Hello World\u0026#34; [-Wcpp] Before C++23 this was a GCC and Clang extension; now it\u0026rsquo;s standard (P2437R1).\n#83. #error - C++98\n1 #error \u0026#34;Hello World\u0026#34; 1 error: #error \u0026#34;Hello World\u0026#34; A standard preprocessor directive from C++98.\n#84. #include \u0026quot;Hello World\u0026quot;\n1 2 #include \u0026#34;Hello World\u0026#34; int main() {} 1 fatal error: Hello World: No such file or directory Another case of output as a side effect of diagnostics, only this time from the preprocessor. The preprocessor looks for a file with this name, doesn\u0026rsquo;t find it, and bails out with a fatal error. A quoted name may contain a space, so \u0026quot;Hello World\u0026quot; is a perfectly legal header. A bit of a hack too, but oh well)\nCompiler-specific #85. #pragma message\n1 #pragma message(\u0026#34;Hello World\u0026#34;) 1 note: \u0026#39;#pragma message: Hello World\u0026#39; Unlike #warning and #error, #pragma message isn\u0026rsquo;t in the standard. It\u0026rsquo;s an extension, supported by GCC, Clang, and MSVC.\n#86. __attribute__((warning(...))) - GCC only\n1 2 3 4 __attribute__((warning(\u0026#34;Hello World\u0026#34;))) void f() {} int main() { f(); } 1 warning: call to \u0026#39;f\u0026#39; declared with attribute warning: Hello World [-Wattribute-warning] There\u0026rsquo;s an interesting technical nuance here that I stumbled upon while testing. This attribute fires only if the call to f() survives to the later compilation stages. At -O0 everything\u0026rsquo;s fine, the warning is there. But at -O2 the compiler inlines the empty f() and drops the call before the attribute gets a chance to fire, so the warning disappears. In other words, whether Hello World appears depends on the optimization level.\n#87. __attribute__((error(...)))\n1 2 3 4 __attribute__((error(\u0026#34;Hello World\u0026#34;))) void f(); int main() { f(); } 1 error: call to \u0026#39;f\u0026#39; declared with attribute error: Hello World Just like warning, but if the call survives to code generation, compilation fails with our message. Unlike #86, I left f() here without a body, because without LTO an undefined function can\u0026rsquo;t be inlined, so the call is guaranteed to survive and the error fires at any optimization level.\n#88. __attribute__((unavailable(\u0026quot;…\u0026quot;)))\n1 2 3 4 __attribute__((unavailable(\u0026#34;Hello World\u0026#34;))) void f(); int main() { f(); } 1 error: \u0026#39;void f()\u0026#39; is unavailable: Hello World unavailable fires at the semantic-analysis level, that is, on any use of the name, so it doesn\u0026rsquo;t depend on optimization.\n#89. __attribute__((diagnose_if(…))) - Clang only\n1 2 3 4 __attribute__((diagnose_if(1, \u0026#34;Hello World\u0026#34;, \u0026#34;warning\u0026#34;))) void f() {} int main() { f(); } 1 warning: Hello World [-Wuser-defined-warnings] Clang lets you attach a conditional diagnostic with custom text to a function. GCC just ignores the attribute (warning: 'diagnose_if' attribute directive ignored).\nAssembler directives #90. asm(\u0026quot;.error …\u0026quot;)\n1 2 asm(\u0026#34;.error \\\u0026#34;Hello World\\\u0026#34;\u0026#34;); int main() {} 1 Error: Hello World The string is printed not by the compiler this time but by GNU as, when it hits the .error directive. Clang with its integrated assembler has the same behavior: error: Hello World.\n#91. asm(\u0026quot;.warning …\u0026quot;)\n1 2 asm(\u0026#34;.warning \\\u0026#34;Hello World\\\u0026#34;\u0026#34;); int main() {} 1 Warning: Hello World The same, but at the warning level: the object file still gets built, the assembler only warns.\n#92. asm(\u0026quot;.print …\u0026quot;)\n1 2 asm(\u0026#34;.print \\\u0026#34;Hello World\\\u0026#34;\u0026#34;); int main() {} 1 Hello World The assembler, unlike the rest of this section, prints the string to stdout, not stderr.\nCompile-time total: 16 methods.\nFinale: all roads lead to write(2) Grand total: 92 ways to print \u0026ldquo;Hello World\\n\u0026rdquo; to the console in C++ on Linux. Of those, 54 are standard C++.\nCategory Count Standard C++26 (runtime) 46 POSIX (+ glibc unlocked) 18 Extensions (BSD/glibc) 6 Linux-only 6 Compile-time (standard C++) 8 Compile-time (non-standard) 8 Total 92 In the end, almost all the runtime methods boil down to a single system call, write(2). And only four have their own system call: writev, sendfile, splice, and io_uring.\nHow many buffers between you and the kernel A separate topic with a lot of confusion around it: how many buffers stand between the call and the kernel:\nMethod Buffering When write(2) actually happens write, writev, dprintf, syscall, asm, /dev/tty none immediately, on every call C stdio: printf, fprintf, puts, fputs, fwrite, putchar, putc, fputc, print, println (and the _unlocked twins) the stdout buffer (FILE*) to a terminal - on every \\n; to a file/pipe - when the buffer is full or on exit iostream: cout \u0026lt;\u0026lt;, .write, .put, sputn, sputc, STL iterators by default - the same stdout buffer; with sync_with_stdio(false) - its own the same, plus an explicit flush / endl In other words, the direct methods write to the kernel immediately, while the buffered ones flush either on \\n (to a terminal), or when the buffer fills up, or during a normal exit from the program (exit flushes all the stdio buffers and runs the destructors of the static cout).\nI\u0026rsquo;d also like to tell you about a nuance with cerr and clog (#24 and #25). It\u0026rsquo;s commonly believed that cerr is unbuffered and clog is buffered.\nstd::cerr has the unitbuf flag set, so it flushes after every output operation. std::clog doesn\u0026rsquo;t have this flag. You\u0026rsquo;d think clog would accumulate output, but by default (sync_with_stdio(true)) both streams write to the C stderr, which is itself unbuffered. So on POSIX platforms both actually write immediately. I checked via strace (the line \u0026quot;Hello\u0026quot; \u0026lt;\u0026lt; \u0026quot; \u0026quot; \u0026lt;\u0026lt; \u0026quot;World\u0026quot; \u0026lt;\u0026lt; \u0026quot;\\n\u0026quot; is 4 operations):\n1 2 strace -e write ./cerr -\u0026gt; 4 separate write(2, …) strace -e write ./clog -\u0026gt; 4 separate write(2, …) The difference appears only if you detach iostream from stdio:\n1 2 3 std::ios_base::sync_with_stdio(false); std::clog \u0026lt;\u0026lt; \u0026#34;Hello\u0026#34; \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; \u0026#34;World\u0026#34; \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // now 1 write(2, \u0026#34;Hello World\\n\u0026#34;, 12) std::cerr \u0026lt;\u0026lt; \u0026#34;Hello\u0026#34; \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; \u0026#34;World\u0026#34; \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // still 4 - unitbuf flushes every time Now clog really does accumulate everything in a buffer and flush it with a single write at the end, while cerr, because of unitbuf, still flushes on every operation.\nIn the end, if you run the write-based methods, strace -e write shows the same result everywhere, up to the descriptor:\n1 2 3 4 5 strace -e write ./cout -\u0026gt; write(1, \u0026#34;Hello World\\n\u0026#34;, 12) strace -e write ./printf -\u0026gt; write(1, \u0026#34;Hello World\\n\u0026#34;, 12) strace -e write ./write -\u0026gt; write(1, \u0026#34;Hello World\\n\u0026#34;, 12) strace -e write ./syscall -\u0026gt; write(1, \u0026#34;Hello World\\n\u0026#34;, 12) strace -e write ./cerr -\u0026gt; write(2, \u0026#34;Hello World\\n\u0026#34;, 12) Even an uncaught throw (#41) ends up simply writing to stderr: (write(2, \u0026quot;terminate called…\u0026quot;, 48), then write(2, \u0026quot;Hello World\u0026quot;, 11)).\nAnd that\u0026rsquo;s the way it is, folks. So I don\u0026rsquo;t understand people who say C++ is bloated. It\u0026rsquo;s all very simple and concise.\n","permalink":"https://niksol15.github.io/blog/en/posts/cpp-hello-world/","summary":"\u003cp\u003eHas it ever happened to you that on a Sunday evening you start being tortured by the question: \u0026ldquo;So just how many ways are there to print \u003ccode\u003e\u0026quot;Hello World\u0026quot;\u003c/code\u003e to the console in C++?\u0026rdquo;.\nI hope not, because that\u0026rsquo;s an occupational hazard at the very least. But I did ask myself exactly that, and I realized there isn\u0026rsquo;t a single source out there\nthat answers it. So I decided to make such a source myself.\u003c/p\u003e","title":"All (92) Ways to Print \"Hello World\" in C++26"},{"content":"Part 1: Program Overview | Part 2: Admission Experience\nWelcome back! In this post I\u0026rsquo;ll cover how the program is organized and talk about my first course - Advanced Operating Systems. I\u0026rsquo;m a massive procrastinator, so I\u0026rsquo;ve accumulated quite a bit of material. Originally I wanted to cover all the courses I\u0026rsquo;ve taken so far in one post, but realized it would be way too long. I\u0026rsquo;ll write about 3 more courses in the next one.\nHow the Program Works Onboarding After Admission All new students are automatically enrolled in a mini-course where the program director Dr. Joyner briefly explains everything: what\u0026rsquo;s expected from students, how the learning process works, what\u0026rsquo;s important to know from the start. So if you get in, you\u0026rsquo;ll definitely understand what\u0026rsquo;s expected of you.\nCourse Registration and Time Tickets Next comes what might be the trickiest part - course registration.\nOMSCS students use the same system as all other Georgia Tech students, but not all courses are available to OMSCS. You can only register for courses once your Time Ticket time arrives. Registration is split into phases - there are 2 in total. New students can only register in the second phase, when most seats are already taken. The Time Ticket times are pretty convenient for EET timezone, so you won\u0026rsquo;t have to wake up in the middle of the night to register.\nThese Time Tickets are assigned automatically, and the time depends on how many courses a student has already completed. The longer you\u0026rsquo;ve been studying, the better your chances of getting into a course - assuming you don\u0026rsquo;t miss your Time Ticket window.\nSeats, Waitlists, and FFF Most courses have limited seats, but there are a few courses with unlimited capacity, so the situation where you can\u0026rsquo;t register for anything at all is practically impossible.\nSome seats are reserved for the waitlist. If you couldn\u0026rsquo;t register for a course directly, you can add yourself to this queue. The first person in line gets limited time to use their opportunity to register. If they don\u0026rsquo;t take it, the opportunity passes to the next person.\nBefore classes start there\u0026rsquo;s \u0026ldquo;Free For All Friday\u0026rdquo;, now called \u0026ldquo;Free For All !Friday\u0026rdquo; because it actually happens on Thursday. During FFAF all reservations are lifted, and you can often snag a spot bypassing the waitlist.\nBecause of this system, it\u0026rsquo;s entirely possible you won\u0026rsquo;t get into the course you want in your first semester because all seats are taken. You need to be prepared for this and have a plan B.\nThere\u0026rsquo;s one course that\u0026rsquo;s nearly impossible to get into unless it\u0026rsquo;s one of your last semesters: Intro to Graduate Algorithms. This happens because this course is required for graduation, and almost all seats are reserved for \u0026ldquo;graduating\u0026rdquo; students. Although I\u0026rsquo;ve heard stories of people getting in during their first semester through FFAF.\nCanvas, Ed Discussion, Lectures, and Assignments A bit about how the learning process is organized.\nYou\u0026rsquo;re added to the course in Canvas and to the forum on Ed Discussion. Lectures are often all available at once and are located in Canvas and/or Ed. There\u0026rsquo;s also always a separate reading list of recommended papers on the topic.\nEd Discussion is where most communication between instructors and students happens, as well as collaboration between students. I\u0026rsquo;ve seen many times how students posted questions and got explanations from both Teaching Assistants and other students. If you prefer live interaction, there are weekly office hours where you can talk to the professor and TAs. They\u0026rsquo;re not always at convenient times, but they\u0026rsquo;re recorded so you can watch them later. To be honest, I never really used this and never attended, only watched the first one in my first semester. The point is that the situation where you\u0026rsquo;re completely lost with nowhere to get help is very unlikely. With some effort and time investment, you can get much more out of a course than just watched lectures.\nAt the beginning of the semester there\u0026rsquo;s always a post where everyone introduces themselves - it\u0026rsquo;s a good way to grow your LinkedIn network with people from around the world. From my experience, most students are from English-speaking countries, mostly the US (what a surprise).\nAvailability of labs/assignments depends on the course, but usually they open gradually throughout the semester.\nThe number of exams depends on the course, but often there are two: midterm and final.\nDeadlines are taken seriously: some instructors don\u0026rsquo;t accept late submissions at all, while others give a short extension with a possible point penalty.\nA bit before mid-semester comes the withdrawal deadline. Before this time you can drop a course, and there won\u0026rsquo;t even be a record in your transcript that you took it. After this date, if you leave the course, you\u0026rsquo;ll have a W in your transcript.\nAcademic Integrity What was unusual for me is that they take academic integrity VERY seriously here. They constantly emphasize this and actually monitor it.\nI\u0026rsquo;ve witnessed situations where people got caught taking labs from the internet or generating them with LLMs. With LLMs, the problem isn\u0026rsquo;t that it\u0026rsquo;s obvious the code was generated, but that the LLM generated code based on what was available online, and they caught people specifically for plagiarism. The sanctions look roughly like this:\n1st violation: 0 on the assignment/exam, inability to receive an A for the course, and inability to drop the course 2nd violation: removed from the course 3rd violation: expelled from the program It might seem strange - why is the inability to drop a course even a punishment? The logic is that this way a student can\u0026rsquo;t just exit the course right after a sanction and \u0026ldquo;reset\u0026rdquo; the consequences. Otherwise, the first violation would cost almost nothing - at most you\u0026rsquo;d have to take another semester to catch up.\nThis approach has its pros and cons. The obvious pro is that it increases the diploma\u0026rsquo;s value, since most students actually completed the program rather than cheating their way through it. The con is that the same assignment, which might be quite limited and specific, is given to a large number of people over several years. I like to overthink things, and my concern here is that, as I understand it, there\u0026rsquo;s always a chance that someone\u0026rsquo;s thought process and problem-solving style will overlap with yours, so there\u0026rsquo;s a chance of false positive plagiarism detection. Fortunately, this hasn\u0026rsquo;t happened to me, but if you go to the subreddit, you\u0026rsquo;ll see plenty of posts where people swear they were falsely accused of plagiarism.\nDespite the above, the situation isn\u0026rsquo;t that bad because there\u0026rsquo;s always a review process where students are given the chance to prove their innocence.\nHow Exams Work (Proctoring) Exams are organized similarly to IELTS.\nUsually you have a week or, for example, Friday plus the weekend to complete an exam. So you can choose a convenient time for yourself. The exam must be taken in a quiet room alone.\nFor closed-notes exams, before starting you need to scan the room to show that you\u0026rsquo;re really alone and that there\u0026rsquo;s nothing in the room that could help you. You also need to scan your passport so another person can\u0026rsquo;t take the exam for you.\nDuring the exam, audio from your microphone, video from your camera, and your screen are recorded. Usually no one is watching you live, but the system flags suspicious moments that an instructor might review later. The exam itself can last 2.5 hours, which is an incredible challenge for fans of short funny videos on the internet.\nThe Actual Learning Fall 2024 - Advanced Operating Systems (AOS) I got lucky and registered for the course I wanted. I chose Advanced Operating Systems because:\nI\u0026rsquo;d like to think I know my way around operating systems, and this is an easy way to get into the groove through a topic I\u0026rsquo;m already familiar with. Usually many people take this course after Graduate Introduction to Operating Systems, so relatively few new students compete for seats in AOS. You might think this course is about operating systems, but that\u0026rsquo;s only partially true) I\u0026rsquo;d call it Introduction to Distributed Systems. Here\u0026rsquo;s what\u0026rsquo;s actually covered:\nThe course starts with a short refresher on basic things - what an OS is, how virtual memory works, cache, multithreading, and so on. Basically everything that\u0026rsquo;s taught in OS during undergrad. After that came a homework where you had to answer several questions, and a simple C multithreading lab so people could understand if they\u0026rsquo;re even ready for this course. The questions are mostly basic, like:\nExplain all the actions from the time a process incurs a page fault to the time it resumes execution. Assume that this is the only runnable process in the entire system.\nbut there were also interesting ones, like:\nExplain page coloring and how it may be used in memory management by an operating system.\nThe concept of page coloring was new to me. Turns out the only more-or-less active kernel that uses it is FreeBSD.\nNext is the first serious topic - OS Structures. Here they examine three approaches to kernel structure: SPIN, Exokernel, and L3 Microkernel. Generally quite interesting, especially considering that microkernel architecture is still alive and used in QNX, which is one of the main choices when you need an RTOS. There was also a very instructive story about how optimization affects software: until a certain point, microkernels were considered slow because in Mach from Carnegie Mellon, border crossing took about 900 CPU cycles. But at some point Liedtke took on the challenge and optimized it in his L3 OS down to 123 cycles. Even though Mach was written by far from stupid people. After that, microkernels were no longer considered slow)\nNext topic - Virtualization. This covers CPU, Memory, and Device Virtualization. Full Virtualization (VMware) and Paravirtualization (Xen) are compared. This module has the first C lab using libvirt. It consists of 2 parts: vCPU scheduler and memory coordinator for guest OSes. I really liked this lab because there\u0026rsquo;s no obvious best deterministic solution. You need to come up with a heuristic so the solution works well enough across several different scenarios.\nAfter that - Parallel Systems, the largest module in the course. There\u0026rsquo;s a lot here. It starts with Symmetric Multiprocessor (SMP) and how memory consistency and cache coherence are implemented. Then various possible ways to implement spinlocks and barriers on UMA and NUMA. Next subtopic is implementation and optimization of Remote Procedure Call (RPC), followed by various scheduling approaches. This large topic concludes with a discussion of how to optimize an OS specifically for Shared Memory Multiprocessor with a large number of CPUs using Tornado as an example. The main idea is to minimize the number of global objects in the kernel protected by a mutex, replacing them with local replicas.\nNext is Distributed Systems. You can\u0026rsquo;t talk about this without mentioning Lamport. This is the person who invented LaTeX, Paxos, TLA+ and received the Turing Award in 2013 for contributions to distributed computing. His paper \u0026ldquo;Time, Clocks, and the Ordering of Events in a Distributed System\u0026rdquo; from 1978 is the foundation of this entire module. The main idea: in a distributed system there\u0026rsquo;s no global time, so you need another way to determine what happened before and what happened after. Hence the happened-before relationship and logical clocks. All of this exists to achieve distributed consensus. As a bonus, they also cover distributed mutual exclusion. Fun fact: Lamport\u0026rsquo;s algorithm for this requires at least 3(N-1) messages per lock, where N is the number of processes. After this, they talk about Active Networks, a concept from the 90s where routers don\u0026rsquo;t just forward packets but can execute code. The 90s were a long time ago, but Active Networks can be considered one of the conceptual predecessors of SDN, although architecturally they differ significantly. Nowadays almost everything is Software Defined, even cars.\nThese two topics have a corresponding second lab. In it you need to implement several SMP barriers using OpenMP and several Distributed Memory barriers using MPI. All of this needs to be benchmarked, and you write a report explaining the results. The cool thing about this lab is that you get access to PACE - Georgia Tech\u0026rsquo;s cluster, where you run jobs on multiple machines inside the cluster through SLURM.\nThen some archaeology called Distributed Objects and Middleware: Spring OS (not to be confused with Spring Framework), Java RMI, Enterprise JavaBeans. Not a very useful module in my opinion, since all of this is already dead.\nAfter that - Distributed Subsystems, applications of distributed systems. There are 3 submodules here. The first is about Global Memory Systems. In short, the idea is to swap not to local storage but to RAM of another machine connected via LAN. As you can tell, this was invented long before SSDs. You might think this is a long-dead idea, but I looked it up, and this idea became the foundation for disaggregated memory, which is now used in HPC clusters. The next submodule is called Distributed Shared Memory, about how you can implicitly present physical memory on different nodes as a single logical address space to a program, hiding explicit communication. The pros are obvious, I think. The cons - you can hang on a memory access waiting for memory from the other end of the cluster. And the last submodule is about implementing a distributed filesystem.\nAround this point you had to do the third lab. In it you need to implement something like a store using gRPC with a threadpool. Generally an interesting lab that\u0026rsquo;s essentially preparation for the final lab. Also interesting: at the time I was working on it, the latest gRPC versions weren\u0026rsquo;t supported, so asynchronous communication couldn\u0026rsquo;t be implemented through callbacks, which meant I had to use CompletionQueue. And the approach here is quite interesting. Each operation has a void* tag. In this tag you usually need to pass some dynamically created object via new, so in subsequent operations you can access it by the tag as a pointer, because you usually need to store some information associated with this chain of calls. It ends up being a state machine with manual memory management.\nNext is Failures and Recovery: LRVM, RioVista, QuickSilver. This module is about how to do transactions and recovery of virtual memory at the OS level with relatively low overhead.\nAfter that - Internet Scale Computing, split into 3 submodules. The first is general, about how to manage resources of a large service and about replication vs partitioning. The second module is about Google\u0026rsquo;s legendary MapReduce. I don\u0026rsquo;t know what to add here, so if you don\u0026rsquo;t know what it is, better look it up. The third submodule is about Content Delivery Networks (CDN) using Coral DHT. This is also a very cool and still relevant technology that\u0026rsquo;s actively used by, for example, streaming services.\nThis module has a corresponding final fourth lab. In it you need to implement a simplified version of MapReduce based on gRPC. The simplification is that workers run on a single host and can use a shared filesystem. The original MapReduce implementation uses Google File System (GFS), which is itself a great example of software engineering.\nThen a brief return to operating systems called Real-Time and Multimedia. In this module they examine TS-Linux and Persistent Temporal Streams, but in practice most of the topic comes down to ideas for implementing real-time scheduling.\nAnd finally some Security - Saltzer and Schroeder\u0026rsquo;s design principles and Andrew File System.\nThat\u0026rsquo;s it for the lectures. A couple words about tests. The course has 3 closed-notes tests: Test 1 (Lessons 1-4), Test 2 (Lessons 5-7), Test 3/Final (Lessons 8-11). Each test has a 3-day window (Friday + weekend). The interesting part is that on Friday they reveal 80% of the questions, so if you want, you can prepare well.\nA special feature of the course is a reading list of ~30 academic papers. Reading everything isn\u0026rsquo;t required, but you need to write summaries for two papers. Personally I only read 3: MapReduce - because I needed it for the lab, and 2 for summaries:\nUsing Processor-Cache Affinity Information in Shared-Memory Multiprocessor Scheduling (1993) - the title speaks for itself) The Multikernel: A New OS Architecture for Scalable Multicore Systems (2009). This one is more interesting. It describes an actually existing OS called Barrelfish, developed at ETH Zürich, whose idea is to be able to work on heterogeneous cores. Imagine: you have a cluster with x86, ARM, and RISC-V, and you run a single OS on top of it. What can I say about this course. I found it interesting, and I think that\u0026rsquo;s what matters most) The labs were especially interesting for me. If you want to watch the lectures, they\u0026rsquo;re freely available online. The cons I can point out are covering outdated technologies, but as they say, a university is not a trade school. Sometimes it\u0026rsquo;s useful to look at things from a purely academic/theoretical perspective, especially considering how some technologies can get a second life - like how Global Memory Systems was reborn as disaggregated memory with the demand for LLM training.\nI really wanted to get an A because it gives access to the course Systems Design for Cloud Computing (SDCC). SDCC is a logical continuation of AOS, with the same professor. The main topic of this course is implementing MapReduce, but in full form. In the end I got an A, but I still haven\u0026rsquo;t taken SDCC because it requires attending meetings that are held at night Kyiv time. It\u0026rsquo;s the only course I know of in OMSCS that isn\u0026rsquo;t fully asynchronous.\nSummary The first semester turned out to be intense. AOS was a good choice for starting - challenging enough to be interesting, but not so much that it would be impossible alongside a full-time job. On OMSCentral, AOS is rated by students at an average difficulty of 4/5.\nBecause I\u0026rsquo;m really bad at time management, I kept putting everything off until the last moment. Since test deadlines were around 8 AM Monday, I took tests several times at 1 AM fueled by massive doses of caffeine. The situation with labs was roughly the same. I want to note that this isn\u0026rsquo;t because the workload is extraordinarily heavy, but purely due to my inability to manage time (there were weeks when I did nothing at all for studying). So if you consistently do something several times a week without long breaks, you\u0026rsquo;ll have no problems combining it with a full-time job.\nWhat\u0026rsquo;s Next? In the next part I\u0026rsquo;ll talk about three more courses: Software Analysis and Testing, High Performance Computing, and High Performance Computer Architecture. Stay tuned! Hopefully the gap between parts will be less than a year this time)\n","permalink":"https://niksol15.github.io/blog/en/posts/omscs-part-3/","summary":"\u003cp\u003e\u003cem\u003e\u003ca href=\"/blog/en/posts/omscs-part-1/\"\u003ePart 1: Program Overview\u003c/a\u003e | \u003ca href=\"/blog/en/posts/omscs-part-2/\"\u003ePart 2: Admission Experience\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eWelcome back! In this post I\u0026rsquo;ll cover how the program is organized and talk about my first course - Advanced Operating Systems. I\u0026rsquo;m a massive procrastinator, so I\u0026rsquo;ve accumulated quite a bit of material. Originally I wanted to cover all the courses I\u0026rsquo;ve taken so far in one post, but realized it would be way too long. I\u0026rsquo;ll write about 3 more courses in the next one.\u003c/p\u003e","title":"OMSCS Part 3: First Semester Experience and AOS"},{"content":"Part 1: Program Overview\nThis post was originally published on DOU in January 2025\nThis is part two of the OMSCS series. Here I\u0026rsquo;ll share my admission experience - from IELTS prep to getting the acceptance letter.\nQuick reminder of what you need for OMSCS admission:\nIELTS Academic or TOEFL iBT at C1 level Bachelor\u0026rsquo;s degree in CS with GPA 3.0+/4.0 (82+/100) 3 recommendation letters Completed application form and paid application fee Timeline The key thing to know is that you need to apply way in advance. I was applying for Fall 2024 semester (starts end of August). The application deadline was March 1st of the same year. If you\u0026rsquo;re applying for Spring semester (starts early January), the deadline is August 15th of the previous year. You can\u0026rsquo;t start in summer, so keep that in mind.\nIELTS Let\u0026rsquo;s start with the most interesting part - English. Here are the requirements:\nTOEFL Requirements\nInternet-based: 100, with minimum section scores of 19 IELTS Academic Requirements\n7.5 Overall Band score 6.5 Reading 6.5 Listening 6.5 Speaking 5.5 Writing I\u0026rsquo;ve always struggled with English. My school transcript literally shows 8/12, and in my freshman year I was placed in the Pre-Intermediate/Intermediate group, but I skipped most of the lectures. There were attempts to fix this, but nothing systematic. So basically, I finished undergrad with solid vocabulary but almost zero grammar. I could speak well enough to express my thoughts, but I was choosing tenses intuitively, almost randomly.\nKnowing I had nearly 9 months until the deadline, I decided to chill for a couple months and start prepping in September. However, I actually started in late September)\nI decided to first improve my general English level, then specifically prep for TOEFL/IELTS. Honestly, this doesn\u0026rsquo;t make much sense - if you\u0026rsquo;re in this situation, just start prepping for the exam while improving your level at the same time.\nI picked First Cambridge Center (not an ad). They assessed my English level at B1+/B2-. I immediately said I needed grammar and wanted individual lessons. Despite this, they first tried to sell me group classes that are more profitable for any online school. I still went to one free trial session. Obviously, it didn\u0026rsquo;t work for me, as group format is better for people who need conversation practice, not for quickly leveling up grammar.\nAfter that I took individual lessons. My review: teachers work from pre-made presentations developed by the school, which limits personalization. Nevertheless, teachers are nice and have solid language knowledge. The IELTS prep didn\u0026rsquo;t really work out though.\nSo in late December 2023 I started prepping specifically for IELTS with another tutor recommended by my girlfriend. In late January 2024 I stopped the First Cambridge Center classes and continued only with the tutor. Overall, this was much cheaper and, most importantly, more effective. I took the actual exam in late February 2024.\nIELTS vs TOEFL The main difference - TOEFL speaking is with a computer, IELTS is with a person. It\u0026rsquo;s a matter of preference, but keep in mind: if you blank out in TOEFL and don\u0026rsquo;t know what to say, that\u0026rsquo;s game over. In IELTS, speaking is conversational format, so if you blank the examiner just asks the next question. Of course blanking affects your score, but not that critically.\nThat\u0026rsquo;s not the only difference between TOEFL and IELTS - you can read more online. But I think the speaking format is the defining factor.\nMore About IELTS Academic Important to understand - high English proficiency doesn\u0026rsquo;t guarantee a high IELTS score. There are 4 sections:\nListening Reading Writing Speaking Each scored 1-9 in 0.5 increments. Listening, Reading, and Writing are taken together, Speaking separately, possibly on a different day (I\u0026rsquo;d recommend booking different days).\nListening 40 questions split into 4 parts:\nPart 1 - everyday dialogue Part 2 - everyday monologue Part 3 - dialogue about education and training Part 4 - academic topic monologue You listen to recordings for 30 minutes with small breaks, then get 10 minutes to check and fill in what you missed. Main challenge is that you listen to each recording only once, so you need to answer on the fly without much time to think.\nPractice is crucial here. This section is easy to train solo - just listen and answer until you learn to do both simultaneously. This is the third hardest section for me.\nReading 40 questions split into 3 sections, each with a text. Total 2150-2750 words. 60 minutes allocated.\nNot much to say really. If you have solid English vocabulary, it shouldn\u0026rsquo;t be a problem. However, I still recommend you doing practice tests a few times before the real thing. This is the fourth hardest section for me.\nWriting Two tasks, 60 minutes total:\nTask 1 - describe a visual: graph, table, diagram, even a map. It\u0026rsquo;s expected to take about 20 minutes. Task 2 - write an essay on a given topic. It\u0026rsquo;s expected to take the remaining 40 minutes. I don\u0026rsquo;t think you can prep well for this without a teacher. The main challenge is that they expect a specific structure. Writing in free format won\u0026rsquo;t get you a high score. That\u0026rsquo;s why you need someone who knows what\u0026rsquo;s expected from you to: 1) explain it to you 2) evaluate your answers and give feedback.\nPractice is key here, you just need to build muscle memory, especially for task 1. Once you\u0026rsquo;ve correctly described one graph, it becomes almost a no-brainer for the next one. This is the second hardest section for me.\nAbout proctoring: before taking these sections, you need to show your entire room on webcam to confirm you\u0026rsquo;re alone. During the exam they capture your screen and webcam video. You are not allowed to use headphones, cover your mouth with your hand, or go off camera. During my exam I propped my chin on my hand, slightly covering my mouth, and someone connected to tell me not to do that. Generally, taking tests in OMSCS using Honorlock is almost identical, except for the part where someone might connect to you.\nSpeaking Three parts, 11-14 minutes total:\nPart 1 - introduction and short interview that includes questions about life, home, work. Generally, it\u0026rsquo;s almost normal conversation 4-5 minutes long. Part 2 - you get a card with a topic to talk about 3-4 minutes. The card includes points you can use with one minute to prepare. Part 3 - discussion of your answer given in part 2 with follow-up questions, taking 4-5 minutes. If you think having good conversational English means you\u0026rsquo;ll definitely score high, then you\u0026rsquo;re wrong. Not only do you need to speak grammatically correct, you also need advanced vocabulary and grammar constructions you wouldn\u0026rsquo;t normally use in conversation.\nHowever, that\u0026rsquo;s not even the main challenge. The main challenge is squeezing out maximum information. For example, if in part 1 they ask a simple question like \u0026ldquo;what\u0026rsquo;s your favorite food\u0026rdquo;, you can\u0026rsquo;t just say \u0026ldquo;Borscht\u0026rdquo; or \u0026ldquo;My favorite food is borscht\u0026rdquo;. You need to come up with 2 sentences on the spot, ideally using some perfect tense. But keep in mind that if you ramble off topic, they\u0026rsquo;ll deduct points for that too.\nTo illustrate: in part 2 they might ask what advice you recently received, but you might not have received any advice at all! Or you can be asked to describe a song you like. How do you even describe a song for 3 minutes? I was lucky, since I got something about environmental pollution, and you can talk about that forever. In part 2 your goal is for them to stop you, not to stop yourself.\nThis was the hardest section for me because I\u0026rsquo;m a straightforward person in conversations and can\u0026rsquo;t really talk about nothing even in my native language.\nRegistration and Results Where to register? I used British Council. Registration includes access to a practice platform.\nOn February 20th I took Speaking, 21st - everything else, 23rd - I already had the results. I recommend taking it early, because IELTS lets you retake individual sections for a fee, so it\u0026rsquo;s better to take the test in advance to have time for retakes if needed. IELTS results valid for 2 years. Also note you need to register early too, because there might not be any slots available.\nResources:\nIELTS 17 Academic (many parts available) IELTS Advantage on YouTube Random mock speaking videos where they do the speaking section on camera My results:\nListening 8.0 Reading 8.5 Writing 6.5 Speaking 6.5 Total 7.5 As you can see, I barely passed, but overall happy with the result, especially knowing where I had started.\nSummary:\nStart prepping early. Prep time depends on your level, but I\u0026rsquo;d allocate around 6 months. Prep with a tutor familiar with the exam format who can develop a program tailored to your needs. Take the exam and register early too, so you have the option to retake if needed. Application After you pass IELTS or TOEFL, everything else is straightforward. Here\u0026rsquo;s the application instruction guide.\nDiploma You need a Bachelor\u0026rsquo;s degree in CS or related field with GPA 3.0+/4.0, which is roughly 82+ on a 100-point scale, but this isn\u0026rsquo;t strictly required. The required part is having a Bachelor\u0026rsquo;s degree, but it can be in almost any field with GPA below 3.0. Such cases are reviewed case-by-case. There are lots of Reddit stories about people in this situation successfully completing OMSCS.\nWhen I was applying, there was zero information about whether it\u0026rsquo;s even possible to get in with a Ukrainian diploma. So I was worried they might not accept mine. But as you can see, those worries were for nothing)\nYou need to upload your diploma copy and diploma supplement. After you pay the application fee and get preliminary approval, you\u0026rsquo;ll need to send the original diploma with supplement and apostilled translations to Georgia Tech. They\u0026rsquo;ll tell you separately when you need to send a physical copy. The diploma deadline isn\u0026rsquo;t the same as the application deadline, so you\u0026rsquo;ll have plenty of time.\nI got lucky as my diploma is bilingual, so I didn\u0026rsquo;t need to do anything extra, just sent it. After they verify your diploma, they send it back.\n3 Recommendation Letters You need to ask 3 people to recommend you, and in the application provide their university emails (if asking professors) or corporate emails (if asking your direct managers) with a description of who they are to you. Then these people get an email with a link to fill out a form.\nHonestly, I don\u0026rsquo;t know if you need to attach the actual recommendation letter there. So when I reached out to professors, I also sent them a letter they could use just in case. I got lucky - only one professor ignored me, meaning I only had to reach out to four in total.\nOne more thing: preferably these should be professors. If you don\u0026rsquo;t have contact with professors anymore, recommendations can be from your technical managers or colleagues who can say something positive about your CS knowledge and skills.\nKeep in mind that people might not respond for a while or take time filling out the recommendation, so don\u0026rsquo;t leave this to the last minute.\nFinishing the Application You\u0026rsquo;ll also need to briefly answer a couple bureaucratic and motivational questions like: why do you even want this program and why do you think you can complete it.\nThen you pay the application fee. When I applied it was $105. I had no problem paying with a Ukrainian bank card online.\nThat\u0026rsquo;s almost it! Then you wait to be \u0026ldquo;processed\u0026rdquo;. Usually they send decisions to everyone around the same time, so you can monitor OMSCS Reddit - people will start posting about getting accepted. Then they\u0026rsquo;ll tell you that you\u0026rsquo;re conditionally accepted, after which you need to confirm your Bachelor\u0026rsquo;s degree by sending it to GaTech (described above).\nIf you have questions, there\u0026rsquo;s an official email you can contact. They\u0026rsquo;ll definitely respond, but be ready for it to take a while due to volume. You can also ask questions on Reddit to get \u0026ldquo;unofficial\u0026rdquo; answers from students and even university representatives.\nAfterword I want Ukrainian IT folks, especially young professionals, to know there are other paths besides defaulting to a Master\u0026rsquo;s at the same place you got your Bachelor\u0026rsquo;s. Today there are opportunities to get world-class education for reasonable money that combines well with full-time work, without leaving Ukraine, that regular people can get into.\nEven if you got your degree long ago - good to know you can retrain or upgrade your qualifications. I\u0026rsquo;ve met many cases of people with 10+ years industry experience going into OMSCS to discover new opportunities or just learn something interesting.\n","permalink":"https://niksol15.github.io/blog/en/posts/omscs-part-2/","summary":"\u003cp\u003e\u003cem\u003e\u003ca href=\"/blog/en/posts/omscs-part-1/\"\u003ePart 1: Program Overview\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eThis post was originally published on DOU in January 2025\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eThis is part two of the OMSCS series. Here I\u0026rsquo;ll share my admission experience - from IELTS prep to getting the acceptance letter.\u003c/p\u003e\n\u003cp\u003eQuick reminder of \u003ca href=\"https://omscs.gatech.edu/admission-criteria\"\u003ewhat you need for OMSCS admission\u003c/a\u003e:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eIELTS Academic or TOEFL iBT at C1 level\u003c/li\u003e\n\u003cli\u003eBachelor\u0026rsquo;s degree in CS with GPA 3.0+/4.0 (82+/100)\u003c/li\u003e\n\u003cli\u003e3 recommendation letters\u003c/li\u003e\n\u003cli\u003eCompleted application form and paid application fee\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"timeline\"\u003eTimeline\u003c/h2\u003e\n\u003cp\u003eThe key thing to know is that you need to apply way in advance. I was applying for Fall 2024 semester (starts end of August). The application \u003ca href=\"https://omscs.gatech.edu/deadlines-decisions-requirements-and-guidelines\"\u003edeadline\u003c/a\u003e was March 1st of the same year. If you\u0026rsquo;re applying for Spring semester (starts early January), the deadline is August 15th of the previous year. You can\u0026rsquo;t start in summer, so keep that in mind.\u003c/p\u003e","title":"Top-10 US University Master's Degree for $7k (Part 2)"},{"content":"This was originally posted on DOU in December 2024\nThis is a series about my experience applying to and studying in OMSCS - the online Computer Science Master\u0026rsquo;s program at Georgia Tech. Here you\u0026rsquo;ll find practical information for anyone considering online CS grad programs, along with plenty of personal experience and subjective opinions.\nThis post covers an overview of online Master\u0026rsquo;s programs and admission requirements.\nWhy OMSCS Back in my freshman year, I stumbled upon an article about a Master\u0026rsquo;s degree that costs less than your daily coffee habit. The author calculated that one day of studying at OMSCS costs about as much as a Starbucks coffee. You have to admit - that\u0026rsquo;s a pretty attractive price tag for an American degree from a well-known university.\nUkrainian CS Education (A Detour) Skip this section if you\u0026rsquo;re not interested in the problems of Ukrainian higher education.\nI earned my CS Bachelor\u0026rsquo;s degree at Taras Shevchenko National University of Kyiv. While I genuinely enjoyed studying in my first years, the later ones were a different story.\nFirstly, I started working on a really cool project at GlobalLogic in my second year. Having spent 3.5 years there, I went from trainee to the meme-worthy \u0026ldquo;21-year-old senior\u0026rdquo; (feel free to comment that 20-year-old seniors don\u0026rsquo;t exist, title inflation is real, and you need at least 10 years of experience to be a true senior). The point is that I could spend my time earning actual money instead of chasing grades for a $50/month scholarship.\nAnd this is super common. Motivated students start working almost from day one because everyone wants to earn money and gain industry experience now, not in 4 years.\nSecondly, as you progress, courses become very specialized, and you can\u0026rsquo;t opt out. You learn what they give you, and everyone gets the same thing regardless of interests. Sure, there\u0026rsquo;s \u0026ldquo;choice\u0026rdquo; - usually between two different names of the same course. At my faculty, the only real choice was picking a department in year 3, which affected maybe 1-3 courses per semester. Your options were:\nchill courses algorithms, ML, cryptography (my pick) web development, but in a classroom setting These two problems feed into each other. When you\u0026rsquo;re forced to take a course you don\u0026rsquo;t care about, there\u0026rsquo;s zero motivation to push through - you just choose to spend time on your job. As a result, most knowledge the university offers just passes most students by (myself included).\nDon\u0026rsquo;t get me wrong, I don\u0026rsquo;t think my faculty was bad. I don\u0026rsquo;t regret getting my Bachelor\u0026rsquo;s there and would choose it again. Firstly, for the networking, as some of the smartest people I know studied there. Secondly, despite everything I described, occasionally there are genuinely interesting courses taught by brilliant professors. The only catch is noticing this before the final exam, when you\u0026rsquo;re scraping by with a 60 because you skipped all the lectures.\nSo while I\u0026rsquo;d pick the same place for my Bachelor\u0026rsquo;s again, I wasn\u0026rsquo;t ready to choose it or any other state university in Ukraine for my Master\u0026rsquo;s. Feedback from former classmates who continued their studies confirmed I had made the right call. I started looking deeper into other options.\nAlternatives My requirements for a Master\u0026rsquo;s program:\nFully online and asynchronous, so I can easily combine it with full-time work Reasonably priced From a well-known, reputable university to strengthen my CV Possible to complete while staying in Ukraine - I have no desire to relocate Wide selection of courses Turns out, there\u0026rsquo;s now a huge number of online Master\u0026rsquo;s programs, especially in the US. OMSCS was the first, and its success sparked many similar programs.\nQuick note on Master of Science in Computer Science - you can check CS program rankings here:\ntopuniversities - worldwide usnews - US graduate programs csranking - US only, but you can filter by specific CS area Generally, most programs require completing around 10 courses at your own pace.\nSo, what do we have:\nStanford - no questions about reputation, but $70k for a degree makes this a non-starter. Which is a shame, because I really enjoyed their MOOCs on cryptography and compilers.\nJohns Hopkins - similar situation, but \u0026ldquo;only\u0026rdquo; $53k.\nUniversity of Illinois Urbana-Champaign (Coursera), aka UIUC MCS. Price drops to $20k-24k - now we\u0026rsquo;re getting somewhere. Plus, a solid selection of 22 courses.\nUniversity of Texas at Austin, known as MSCSO. The price is $10k for 10 courses. However, it has more limited selection of 18 courses. They also have specialized programs for Data Science: MSDSO and AI: MSAIO.\nGaTech, known as OMSCS. And here we hit the jackpot. This is the oldest, largest online MSCS program. About $7k for the entire degree. Nearly 70 courses to choose from!\nThis list isn\u0026rsquo;t exhaustive - there are many other programs. Check Coursera and EdX for more options. For choosing, I recommend Reddit reviews and YouTube videos.\nAmong these, the most popular programs right now are OMSCS, MSCSO, and UIUC. Considering price and course variety, OMSCS was the obvious choice for me, with MSCSO as runner-up.\nWorth noting that if I were specifically into ML/DS/AI, I might have picked UT Austin because:\nIt\u0026rsquo;s one of the top universities specifically for DS Course variety matters less when you\u0026rsquo;re studying exactly what interests you A $3k difference isn\u0026rsquo;t that significant for an entire program About OMSCS and Admission Requirements Besides OMSCS, GaTech also offers separate programs in data analytics OMSA and cybersecurity OMSC. However, I\u0026rsquo;ll focus specifically on OMSCS. I highly recommend browsing the internet about OMSCS beforehand, especially Reddit.\nThere\u0026rsquo;s a huge selection of courses. You need to complete 10, but you can\u0026rsquo;t just pick any random 10. After completing them, you must satisfy the requirements of one of 6 specializations. The easiest way to figure this out is to plan your courses in this planner.\nThe order matters too. In your first year, you need to pass 2 foundational courses (marked with an asterisk in the list) with at least a B. If you don\u0026rsquo;t, you\u0026rsquo;ll just be restricted in course selection until you meet this requirement.\nAdditional details: an academic year has 3 semesters - Spring, Summer, Fall. You can\u0026rsquo;t skip more than 2 consecutive semesters. You have 5 years to complete the program. Everyone recommends starting with 1 course per semester. In my experience, that’s solid advice.\nHopefully I\u0026rsquo;ve sold you on the program, so let\u0026rsquo;s talk about admission requirements. They\u0026rsquo;re described here.\nBachelor\u0026rsquo;s degree in CS or a related field with a GPA of 3.0+/4.0. BUT! Don\u0026rsquo;t stress if you don\u0026rsquo;t meet this exactly - plenty of OMSCS students don\u0026rsquo;t have a CS background, or have a GPA below 3.0. Such applications are just reviewed case-by-case.\nIELTS or TOEFL at C1 level. More details here. Unfortunately, this one is strict and there\u0026rsquo;s no way around it.\n3 recommendation letters. Preferably from professors who taught you during undergrad. If you\u0026rsquo;re an industry veteran who got your Bachelor\u0026rsquo;s ages ago, letters from your managers work too. The key point is that they should praise your CS knowledge and skills, not your willingness to fix broken prod at 3 AM.\nResume and application form with some questions, but that\u0026rsquo;s just paperwork.\nThat\u0026rsquo;s it! If you meet these requirements and pay the symbolic application fee (around $70), you\u0026rsquo;re pretty much guaranteed admission. There\u0026rsquo;s almost no selection process, it\u0026rsquo;s like a massive MOOC where people simply drop out along the way.\nImportant fact: upon completion, you receive a diploma identical to what GaTech gives on-site students. GaTech also emphasizes that the online program is no different from on-site, and you\u0026rsquo;ll need to put in serious effort to graduate.\n","permalink":"https://niksol15.github.io/blog/en/posts/omscs-part-1/","summary":"\u003cp\u003e\u003cem\u003eThis was originally posted on DOU in December 2024\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eThis is a series about my experience applying to and studying in OMSCS - the online Computer Science Master\u0026rsquo;s program at Georgia Tech. Here you\u0026rsquo;ll find practical information for anyone considering online CS grad programs, along with plenty of personal experience and subjective opinions.\u003c/p\u003e\n\u003cp\u003eThis post covers an overview of online Master\u0026rsquo;s programs and admission requirements.\u003c/p\u003e\n\u003ch2 id=\"why-omscs\"\u003eWhy OMSCS\u003c/h2\u003e\n\u003cp\u003eBack in my freshman year, I stumbled upon an article about a Master\u0026rsquo;s degree that costs less than your daily coffee habit. The author calculated that one day of studying at OMSCS costs about as much as a Starbucks coffee. You have to admit - that\u0026rsquo;s a pretty attractive price tag for an American degree from a well-known university.\u003c/p\u003e","title":"Top-10 US University Master's Degree for $7k (Part 1)"},{"content":"Hey, I\u0026rsquo;m Nikita Solonko - a C++ developer based in Chernihiv, Ukraine.\nCurrently working at Atto Trading on low-latency HFT platform development. Before that, I spent three and a half years at GlobalLogic building safety-critical automotive middleware for Volvo/Polestar.\nI\u0026rsquo;m also pursuing a Master\u0026rsquo;s online at Georgia Tech (OMSCS). Got my Bachelor\u0026rsquo;s from Taras Shevchenko National University of Kyiv, Faculty of Computer Science and Cybernetics. My thesis was on elliptic curve cryptography. I have Stanford certificates in compilers and cryptography - I like deepening my CS foundations when time allows.\nWhat this blog is about This blog is an attempt to organize my knowledge and experience through creating content. I started it because I wanted to contribute more content on low-level C++, systems programming, and computer architecture. I also write about my OMSCS experience.\nContact GitHub: Niksol15 LinkedIn: nikita-solonko Blog source code is on GitHub.\n","permalink":"https://niksol15.github.io/blog/en/about/","summary":"\u003cp\u003eHey, I\u0026rsquo;m Nikita Solonko - a C++ developer based in Chernihiv, Ukraine.\u003c/p\u003e\n\u003cp\u003eCurrently working at Atto Trading on low-latency HFT platform development.\nBefore that, I spent three and a half years at GlobalLogic building safety-critical automotive middleware for Volvo/Polestar.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;m also pursuing a Master\u0026rsquo;s online at Georgia Tech (OMSCS).\nGot my Bachelor\u0026rsquo;s from Taras Shevchenko National University of Kyiv, Faculty of Computer Science and Cybernetics.\nMy thesis was on elliptic curve cryptography.\nI have Stanford certificates in compilers and cryptography - I like deepening my CS foundations when time allows.\u003c/p\u003e","title":"About"}]