HNHacker News
TopNewBestAskShowJobs

ynik

1,017 karma · joined October 25, 2013

submissionscomments
ynik··on The Marvin Attack
Even assembly doesn't work anymore, as CPU manufacturers no longer guarantee constant-time execution unless the software opts-in to constant-time mode: https://lore.kernel.org/lkml/YwgCrqutxmX0W72r@gmail.com/
ynik··on How many lines of C it takes to execute a + b in Python
That depends on the hash table implementation and the distribution of the integers.

For the commonly used hash tables with prime size that use modulo to turn the hash code into a slot index, an identity hash for integers is usually fine (unless many integers are multiples of the prime size).

But other hash tables use power-of-two size to replace the modulo operation with a faster bit-and operation. Now an identity hash for integers is much more problematic, e.g. if all integers are multiples of 1000, only 1/8th of the table slots can be used.

The latter kind of hash tables would like all bits in the hash value to be well-distributed; and this is typically not true of the underlying integers. So an additional mixing operation needs to be used. Whether that mixing happens in the hash function or in the hash table depends on the implementation (for some, it's even configurable, e.g. is_avalanching marker in ankerl::unordered_dense).

ynik··on How many lines of C it takes to execute a + b in Python
AFAIK Python strings cache their hash value. All the hash table lookups for object properties should be compile-time constant strings that reuse the string object, and thus also reuse the cached hash value. It may take 11 hash computations for the first print("hello world"), but the second call shouldn't take any.
ynik··on Bjarne Stroustrup Quotes
You can.

The last byte of an emoji is never the same as 'a'. UTF-8 is self-synchronizing, a trailing byte can never be misinterpreted as the start of a new codepoint.

This makes `memmem()` a valid substring search on UTF-8! With most legacy multi-byte encodings this would fail, but with UTF-8 it works!

ynik··on std::source_location doesn't work if you need strings to be types
But it's quite unclear what they are currently doing. Literally translating what you are describing:

   template<char... FILE>
   struct magic { };

   magic<__FILE__> m;
This does not compile with gcc, and std::source_location fails to work for exactly the same reason. I don't see any additional problems with std::source_location that don't equally apply to __FILE__.
ynik··on std::source_location doesn't work if you need strings to be types
If the `const char*` is a compile-time value, you can get its size via `std::char_traits<char>::length()`.

Here I could get `magic_functor<std::source_location::file_name()>::type` to work on MSVC: https://godbolt.org/z/Ph8dd78fa gcc doesn't accept this code, but that seems more like an implementation problem than a specification problem?

ynik··on std::source_location doesn't work if you need strings to be types
`std::source_location` does not have to be a function argument.

It can also be used in an NSDMI, in which case it ends up being instantiated whenever the NSDMI is used, i.e. at every aggregate initialization site.

https://godbolt.org/z/v5rhjqdbY

Of course, that doesn't really allow you to do anything that you couldn't already do in the with a constexpr function (see below). In particular, such a struct can be used as a non-type template parameter, but a `template<NSDMI sloc = NSDMI{}> void foo();` will merely report the line where the template is defined, not where `foo` is called.

    // Results in a compile-time constant, so can be used everywhere where __LINE__ could be used.
    constexpr int line(std::source_location loc = std::source_location::current()) { 
       return loc.line();
    }
So std::source_location can fully replace __FILE__/__LINE__, but it cannot always replace the macro where __FILE__/__LINE__ were being used.
ynik··on Starship will attempt a launch this Friday
> generally when two satellites collide some debris can be shot at a higher orbit which will take forever (or will not) reenter atmosphere.

The lowest point of the new orbit (perigee) is guaranteed to be no higher than the point of collision. Fragments after the collision have no further propulsion, so their new orbit must initially include the point of collision, and can only decay from there. This can also be seen on your scatterplot.

Moreover, the time to decay is most strongly influenced by the perigee, as the atmosphere is the strongest there. If a satellite on an low orbit (decay in decades) explodes, those fragments that "reach a higher orbit" will still (due to the low perigee) decay back to a near-circular orbit in decades. A collision can increase the lifetime for some of its fragments, but not by multiple orders of magnitude (unlike a circular higher orbit).

Your video link also nicely shows that fragments with a low perigee will decay quickly, no matter how high their apogee is.

ynik··on Windows 11 Update 23H2 is stealing users' IMAP credentials
Stop using Windows. It is foolish to assume that any data on a Windows machine can stay out of the Microsoft cloud.

E.g. Microsoft Edge on first launch can import bookmarks+stored passwords from Firefox (AFAIK without any user interaction, unless I clicked without thinking), and it also defaults to uploading this data to the Microsoft cloud (unless you're using a local account?).

ynik··on Java JEP 461: Stream Gatherers
That's a common feature nowadays, not unique to Java.

C# has had `enumerable.AsParallel()` since .NET 4.0 (2010). Rust has rayon `input.par_iter()`.

ynik··on Confusing Git Terminology
Making an svn branch and scrapping it, indeed never was a big deal.

Making an svn branch and merging it, now that was a huge issue. "svn merge" sucked compared to "git merge".

For the project I was managing back in 2007 I started using git-svn, because importing Subversion commits into git, using "git merge", and then exporting commits back to the subversion server, was faster and worked better than "svn merge" did (back in 2007).

ynik··on Confusing Git Terminology
The way I got into git was: I was working on a project using Subversion and wanted to be able to work on train rides (and internet connectivity on trains wasn't a thing back in 2007). Initially I tried SVK, but I found that git-svn actually worked better.

In fact, it worked so well, I stopped using "svn merge" (which took 5+ minutes on our repository for every merge), and started using "git merge" with git-svn instead (which reduced the merge time to <3s, and even the extra git<->svn sync overhead cost only 30s or so). As a bonus, git also reported fewer "merge conflicts" (svn at the time had issues repeatedly merging from a branch to trunk).

So when I ended up picking a DVCS for another project, git was the natural choice since I already knew it. I imagine there are a lot of developers who started out on SVN and took a similar route to learning git, so having a high-quality Subversion bridge turned out to be one of the critical features on the road to adoption. This advantage in adoption then snowballed via forges like GitHub.

ynik··on MAME 0.260
python3 in 2008 was practically unusable. It was incompatible, none of the libraries on pypi supported it; and it was extremely hard to write a code base that was compatible with both python2.6 and python3.0 at the same time, so almost none of the library authors bothered to add python3 support.

In 2010, python2.7 was released, backporting a bunch of python3 features. In 2012, python3.3 was released, fixing a bunch of needless incompatibilities. This finally made it viable to support both 2.7 and 3.3 in the same code base. For the most part, the migration of the Python library ecosystem only started in/after 2012.

python3 wasn't really a viable choice for application development until most libraries were ported (2014? 2015?).

ynik··on Progress on No-GIL CPython
In fact, it prevents very few race condition bugs.

Even inside a C extension where the Python API feels like it gives you control over when you release the GIL (with functions you'd have to call explicitly to release the GIL), it turns out that:

* any operation that allocates new Python objects might trigger garbage collection

* garbage collection may run `__del__` of objects completely unrelated to the currently running C code

* `__del__` can be implemented in python, thus releasing the GIL between bytecode instructions

Thus there's a lot of (rarely exercised) potential for concurrency even in C extensions that don't explicitly release the GIL themselves. nogil will make it easier to trigger data race bugs, but many of them will already have been theoretically possible before.

ynik··on Getaddrinfo() on glibc calls getenv(), oh boy
The standard libraries on Windows don't involve libc. The Windows APIs look rather different, and in general are much more friendly to multi-threading. POSIX on the other hand tends to assume that the program is in control of everything happening inside of it, which is an incorrect assumption due to libraries.

In this particular case, the Windows APIs have neither getaddrinfo() nor getenv(); and the closest equivalent GetEnvironmentVariableW is perfectly thread-safe. Microsoft additionally has a C runtime (msvcrt) providing functions like getenv(), but this is much less fundamental than it is on other system. Every program is supposed to ship its own copy of the C runtime, it's not officially part of Windows! And it's perfectly possible for multiple different copies of the C runtime to be loaded into the same Windows process. And since *environ is a variable defined by the C runtime, there's a different copy for each C runtime...

ynik··on How I Made a Heap Overflow in Curl
Yesterday's Windows 10 security updates did not include a new curl version; the curl included with Windows is still 8.0.1.
ynik··on The novel HTTP/2 'Rapid Reset' DDoS attack
I wonder why exactly this attack can't be pulled off with HTTP/1.1 and TCP RST for cancellation. It seems that (even with SYN cookies involved) an attacker could create new connections, send HTTP request, then quickly after send a RST.

Is it just that the kernel doesn't really communicate TCP RST all that well to the application, so the HTTP server continues to count the connection against the "open connection limit" even though it isn't open anymore?

ynik··on Calling Rust from Python
Right now FFI + ctypes has one big advantage: it supports true parallelism with GIL-per-subinterpreter in Python 3.12. AFAIK all the higher-level binding libraries/tools (PyO3, pybind11, nanobind, Cython) don't support this yet; and in fact can't really support it without API-breaking fundamental design changes.
ynik··on Calling Rust from Python
The Python<'_> type appearing as a parameter means the caller has already acquired the GIL for you. The GIL is held when Python calls into extension modules, and PyO3 doesn't automatically release it (but you can do so yourself).

It's essentially a dummy parameter that serves as proof that the GIL is held. You need to provide it to PyO3 when calling back into the Python runtime, that way the compiler ensures the GIL is still held where required.

ynik··on Microsoft Defender was flagging Tor browser as a trojan and removing it
Fun fact: NTFS on C: is extra slow.

A Python script creating 10000 empty files takes 1.2s on C:, but only 0.5s on D:. Both are NTFS partitions on the same SSD, both partitions are using the default settings. (but the defaults differ: on the system drive Windows enables compatibility features such as 8dot3names, on additional partitions it does not)

ynik··on Python 3.12
Because in our case, all threads would be running a mixture of Python and C++. Our core data structure is implemented in C++, large (often >10 GB), and can be shared across threads (thread safe, usage is mostly read-only). But we have lots of analysis algorithms accessing that data structure, most implemented in Python. We tried releasing the GIL for every tiny call to C++, but that approach can barely keep two cores busy due to constant fighting over the GIL. (there's no good "inner loop" that could avoid touching the GIL within the loop body)

Rewriting most/all of the Python analyses in a different (GIL-free language) is a no-go, the analyses have accumulated over the years and now there's more than a thousand of them. It would consume all our development resources for the next ~5 years. In retrospect I can say that choosing Python for these was major mistake, but it's one that cannot be fixed without a company-killing rewrite :(

We actually invested several months of developer time in allocating our core data structure in shared memory, allowing us to parallelize with multiprocessing. But there's still a whole bunch of ancillary data structures written in C++ that are not so easy to put in shared memory, so all analyses touching those are limited to a single process, which by Amdahl's law immediately starting dominating our execution time.

ynik··on Python 3.12
However if one is using C++/Rust anyways, then it's a good idea to stay away from Cython. From afar, Cython seems like a viable solution for Python/C++ interop. But the details get messy: you need to clone the .h headers into .pxd Cython-readable headers; and more advanced template-magic C++ constructs may end up being not directly usable in Cython due to missing features or bugs in the C++ support.

In the end, we ended up with quite a number of layers wrapping each other:

  1. actual C++ implementation
  2. actual C++ header
  3. C++ wrapper implementation, avoiding constructs that Cython doesn't support
  4. C++ wrapper header
  5. Cython .pxd for step 4
  6. Cython .pyx exposing `cdef class`es to Python with a nice Python-style API for the original C++ library.
  7. Hand-written .pyi for type checking the remaining Python code, because Cython doesn't have support for auto-generating these yet.
Had we used pybind11 / nanobind instead, we could have stopped at step 3. Cython started easy, but ended up being a major maintenance burden.
ynik··on Python 3.12
Not for pure python code; but there's massive advantages for mixed C(++) and Python: I can now have multiple sub interpreters running concurrently and accessing the same shared state in a thread-safe C++ library.

Previously this required rewriting the whole C++ library to support either pickling (multiplying the total memory consumption by the number of cores), or support allocating everything in shared memory (which means normal C++ types like `std::string` are unusable, need to switch e.g. to boost::interprocess).

Now is sufficient to pickle a pointer to a C++ object as an integer, and it'll still be a valid pointer in the other subinterpreter.

ynik··on What it takes to pass a file path to a Windows API in C++
"2 extra calls" is a weird metric here. Some calls are vastly more expensive than others. Syscalls come with a significant cost, encoding conversion of short strings (esp. filenames) does not. Hiding just the syscalls behind an abstraction layer is vastly simpler than doing that and additionally hiding the string representation, so "UTF-8 everywhere" is IMHO the right solution.
ynik··on Memory-efficient enum arrays in Zig
Turing completeness doesn't have to come from a single feature, it often comes from a combination of features.

e.g. the interaction between subtyping (e.g. inheritance) and generics (with variance) is tricky: https://www.cis.upenn.edu/~bcpierce/papers/variance.pdf

It can be highly nontrivial to tell if a language actually has a Turing complete type system: the 2007 Kennedy&Pierce paper made it likely that java was turing complete; but it took until 2016 until it was finally proven that to be turing complete (https://arxiv.org/abs/1605.05274).

> What would it take a for a type system to NOT become Turing complete?

An analysis of all possible interactions between all features in the type system, building a formal proof that the type system is not turing complete. This is not really realistic for the style of complex generic type systems that programmers are now used to, it would need to be a vastly simpler language.

ynik··on Removing garbage collection from the Rust language (2013)
A GC-free systems language isn't just for OS kernels. Rust's main use case was to replace C++ components in firefox. This wouldn't have been possible with a language that brings along its own GC, as Firefox already has a JS GC. Multiple garbage collectors in the same process are a quick way to madness, especially if you have reference cycles stretching across multiple GC heaps. This also comes up when writing Python extension modules, base libraries that are meant to be usable across languages, etc...

This is part of why Rust is so successful -- it's the first real alternative for this space since C++ came along. For most application development, it's better to use a garbage collected language. But in the application space there is a much bigger choice of languages already available, Rust wouldn't have been a big deal over there.

ynik··on Using uninitialized memory for fun and profit (2008)
How do you decide whether an UB-optimization is insane or not? "I know it when I see it" may work for lawmakers, but it's not good enough for compiler developers.

Because if you want to disable all UB-based optimizations, there's already a compiler flag that does that: -O0.

Seriously, in a language as low level as C++, every optimization needs some form of assumptions, and UB is how we currently allow compilers to make those assumptions.

Example: because it's undefined behavior to read pointers out-of-bounds, it is not possible for a valid program to scan a whole stack frame, so it is okay for the compiler to remove variables from the stack frame and instead store them in registers.

Without UB-optimizations, you would need some others justification of why it is okay for the compiler to change the result of a stack scan. Or use -O0 which keeps all variables on the stack. (but that certainly will be slower!)

ynik··on Using uninitialized memory for fun and profit (2008)
At least in clang, your variant is not any better: uninitialized reads return the special value `undef`, which the compiler can constant-propagate to both uses of `sparse_val`. But then each `undef` can turn to a different value at each use, even if they both came from the same uninitialized read.
ynik··on Using uninitialized memory for fun and profit (2008)
It's because accessing uninitialized memory is not UB in clang.

From clang's point of view, a read from uninitialized memory returns the special value `undef`. Many operations applied to `undef` also return `undef`, but some (e.g. branching `if (undef)`) are outright UB. But the original example doesn't do anything with `undef`, so it's not clang-UB. The compiler generated valid code for the function, it just happens to pick "whatever was previously in RAX" as the value read from the uninitialized memory. The malloc call is optimized out because it's no longer necessary after the uninitialized read was replaced (the same will happen to any other malloc call where the return value is unused).

Note that the reason for this weird `undef` semantics ("almost but not quite UB yet") is that padding bytes are also uninitialized, but copying a whole struct incl. padding (even a memcpy-style byte-wise copy) must be defined behavior.

ynik··on Compiling Rust for .NET, using only tea and stubbornness
Yes, I supposed you could compile rust to .NET if you boxed everything (including simple integer fields, to allow taking their address as &i32) and used inheritance hierarchies for enums. Though that approach will still fail as soon as you encounter unsafe code with pointer casts, so I doubt you would be able to use the standard library.
← PreviousPage 3 of 8Next →