“To be safe allocate 5 bytes more than you need”
twitter.com
twitter.com
This can lead to a pretty subtle crash because, except in msan mode, reading those fifteen bytes won't cause any problem except at the page boundary. This is because you don't corrupt the sentinel bytes, since they're never written. So to detect the bug you either have to run the code in debug mode (which we rarely do, because the code is deep inside a very expensive database system), or you have to get unlucky.
I tried fixing this, but adding a "past the end" test really hurt performance. Instead, we continued with the preexisting solution: copy the data into a buffer fifteen bytes longer than the data, then decode it. T_T
https://github.com/pedrocr/rawloader/blob/c793a132fa03e336f8...
Most modern languages no longer have preprocessors, which is a pity.
In cases like these, you want complex computed constants. You want those computed at build time, and not at runtime. I can:
* Run the math on a whiteboard, compute +5 or +15, hardcode that, and have the world explode when the code changes
* Run the math in code, at runtime, and have the system be slow.
* Code up the math, have it run at build time, and have a clear documentation of what the constant is, why, how it's computed, and have it change automatically if the code changes.
There were awesome things done in C++ templates back in the day, with some pretty magical performance. The preprocessor would unroll loops to the appropriate level for a given processor, align things to cache boundaries, and similar.
I think the reason this might have gone away is that with GHz CPUs, few people care as much about performance anymore. Most code is no longer CPU-bound, but programmer-bound, and lower cognitive complexity is more important than better performance.
I love compiler sanity checks of math operations with static asserts, and stuff like compile checking for ub and cdb.
constexpr int signed_int_fixed_yet(){ // compiler error return std::limits<int>::max +int(1); }
tuplify<PodType>() is wonderful too.
Just a shame it adds so much boilerplate though, like why couldnt every function have the same constexpr if possible property as lambdas, and is so damned slow and costly to compile. I also keep writing half of a nice interface, then running into template recursion depth, or gcc failing to compile correctly/(identically to clang).
Its also still a bit too limited. Like, consider the case where everything is known compile time: int a=5; // lets say sizeof(a) =4 a<<64; // should be compile error, not ub for(int i=0;i<64;++i) a<<i; // should be compile error, // note that either line above can be removed by the compiler. Leaving a=5;
template<class T, int R,int C> Matrix{ std::array<T,RC> data; // constexper // should be automatic T operator(int row, int col){ if is_constexpr_in_context(row) // now wouldnt this be nice... static_assert(row<R,""); // only if row is constexpr argument return data[rowC+col]; } };
Matrix<double,3,4> m; auto a=m(1,2) // row is known compiletime, compiler will warn, should be compile error m(a+3,0) // also known compiletime(array default init to 0), should warn, should be compile error for(int r=0;r<3;++r) for(int c=0;c<6;++c) a+=m(r,c); // oob known compiletime, should warn...
Its not like this is that hard either, just transform the statements to use m.at<r,c>() and it works, so its just the compiler identifying constexpr context automatically as for lambda and replacing functions with their argument templated equivalents, and autogenerating these functions. It cant fix everything, but the number of errors people made when using such libs would massively decrease, especially since templated matrix classes are almost universally have visible definitions, and it often applies.
It is UB for signed integers. There is a lot of stuff from c inside c++ :-).
* Code up a unit test that replicates end of buffer scenarios and verifies that X bytes is enough and X-1 bytes fails.
These build modes catch the vast majority of undefined behavior, and with relatively low performance overhead. If you're not running your tests with sanitizers enabled, you're missing out on a nearly free way to locate and eliminate an entire class of bugs.
This was a popular one for a while, back when C++ was still used for major new applications: https://en.wikipedia.org/wiki/BoundsChecker
There are lots of techniques for this. For example, you can instrument malloc() to allocate 10 extra bytes, put a specific known value there, and confirm it didn't change.
There are much fancier ones too, which make clever use of hardware features like MXP. It's a whole research field.
m4 is a preprocessor for all languages.
Just put it in the source code, and if all the information required to calculate the constant is present at compile time - why, then the compiler should calculate it, and optimize away all that hairy math! No need for a separate "preprocessing" step, or a hairy template language.
There are whole classes of problems one runs into from:
- The ability to change variables by reference or through introspection
- Exceptions in intermediate computations
- Etc.
The set of transformations one can do while maintaining guaranteed equivalence is limited.
https://github.com/protocolbuffers/protobuf/blob/master/src/...
Protobufs are full of varints, and we want to perform only one bounds-check per field. So we ensure that the input buffer always has at least 16 bytes available to parse per field. If there are fewer than 16 bytes remaining, we copy these last few bytes of data into a patch buffer and parse from that instead.
https://github.com/protocolbuffers/protobuf/blob/5a7a4a52a79...
That's only true if the strategy avoids hitting an invalid page.
Reading a single uint64_t which is aligned on an 8 byte boundary will be fine. If any byte of that is within a valid page, the other bytes have to be.
If two uint64_t's are read, then as a whole, they have to be a 16-byte-aligned block for that same assurance to hold.
If the code is using misaligned reads, that itself will crash on some processor architectures, or possibly trigger expensive handling.
It would very occasionally crash during a draw that reads the last few bytes of a vertex buffer on AMD cards only.
Very annoying to find because it only triggered rarely.
Our solution was the same, just allocate vertex buffers a little longer on AMD cards and don’t use the extra space.
What I’m getting at is that it likely wasn’t a driver bug. The other drivers were just more tolerant of the incorrect behavior.
It's the downside of malloc not knowing the type you want and returning void*. It has to assume the worst alignment requirements.
Which actually happen to be 128 bytes for SSE instructions.
Maybe someone should open source an efficient yet safe varint writer?
for (int i = 0; i<n-15; ++I) {
dec.FastUnsafeRead(result);
}
for (int j = n-15; j < n; ++j) {
dec.SlowSafeRead(result);
}You might also be able to heuristically compare buffer address modulo the page size to N to know whether you need to be safe or not.
Note: you still have to be careful if your entire input is less than 16 bytes, there can be other alignment issues, etc..., but it _might_ be better in your situation than a similar fix that other comments have mentioned which applies the fast read to all but the last partial chunk and then finishes the tail with some slow cleanup code.
Can't you call it n-1 times and then handle the last 0-16 bytes a little slower?
I first learned about it when valgrind complained at every place I was calling strlen.
Such behavior is not crazy but this decoder does not take a normal array as a type at that point. So describing it as such can result in other programmers providing an array that does not follow the expectations of this decoding function.
-edit I will say if you have length information you should be able to use the fast method on all but the last chunk without checking on every data chunk.
Without these kinds of overruns many vectorized algorithms would not be possible, so you'd probably see a noticeable performance decrease.
uint64_t is 8 bytes, not 16. Why are you overreading up to 15?
How did you conclude that the test hurt performance? I have researched this for over a decade and my conclusion is that the bounds check is for all intents and purposes free, because it is hidden in the latency of the actual memory access (even if it comes from L1 cache). The only way the bounds check can ever hurt performance is if it adds additional memory reads.
That would indicate that you don't have a memory access for the upper bound at all currently, which sounds like you could overread an arbitrary amount, not just up to 15 bytes.
https://news.ycombinator.com/item?id=12071686
[0] Because there are only 8 bits to store the axle counter for whether the train has passed, and you don't want overflow to zero.
If you're worried about double-counting an axle you probably need to design your axle counters better.
(Did a bit of research, at least one has existed: http://cs.trains.com/trn/f/111/t/72627.aspx)
In the steam era, things were rather different. Steam power requires a linkage of rather large drive wheels, which generally means you need unpowered leading and trailing wheelsets for stability and performance purposes. The trailing wheels are almost purely to support the weight on the back end of the locomotive, but leading wheels also help with curves in addition to supporting weight on the front end. Single leading axles were quite common in the steam era, with 2-8-0 being perhaps the single most common arrangement.
It seems like you'd want to do something like: (max axles) = 256 - (% of missing an axle) * (number of axles)
More or less. (Obvs you'd have to know the chance of missing an axle - but without a reasonable guess at this it seems you couldn't really set the max value accurately)
At some point I have realised, that just letting cJSON be the horror of a codebase it was before I started maintaining it would have been the best thing to do, but that's what you call hindsight.
At least now I know.
One reason I burned out was because I felt horrified by how many people are using cJSON and felt a responsibility to make it better and safer for all of them, even though I myself haven't ever been really using it. The combination of this mounting pressure plus the the impossible fight against semantic versioning and memory unsafety gave me the rest.
By improving it, but being unable to completely "fix" it, I probably led more people to use it, making the situation even worse than it was before.
What needs to be fixed so that this "+5" can be removed? What happens if something changes and the magic number needs to be 6? Also, where's the unit test?
What would you think if the following were true for a fairly generalized problem:
- It is easy to prove a particular upper bound on memory requirements when calling a function.
- It is very difficult to know exactly what the memory requirements will be for particular input.
- No known input achieves the known upper bound.
Is it a problem if we allocate enough memory to satisfy the bound we can prove, and then don't have a unit test showing we can't do with less?
At first glance the example seems contrived to me. Assuming that the code is deterministic, wouldn't it be trivial to measure the memory requirement for a particular input?
What were you trying to ask?
That way you can trust the estimator to always tell you at most how much memory you'd need.
> - It is easy to prove a particular upper bound on memory requirements
That they should have included said proof in the source code (in a comment if the type system isn't expressive enough to run it).
You can certainly fix known knowns, but you can't fix unknown unknowns. Sometimes its safer to simply apply a rule that you have confidence will work everywhere than fix that may introduce unsuspecting problems.
Once you identify a root cause, you can assess the scope. "allocate 5 bytes more than you need" is a pretty broad reaching fix. I'd be very hesitant to implement it without proper time to fix.
The comment you originally replied to has a direct link to the (commit that added the) code in question.
When you program in C, you should know exactly what each byte of memory is for. If you don't do that, not only you will eventually get crashes and leaks and overflows, but you also miss the point of programming in C in the first place.
C is fast, but C is not fast because it overclocks your CPU or whatever magic you can think of. An good reason why is is fast is that it gives you tight control over your memory. You can reuse buffers, size them precisely, allocate them exactly when you need them and free them when you are finished, nothing funny happening behind your back. It generally results in lower memory usage, better use of cache and therefore better performance.
If you don't manage your memory precisely, then sure, C is probably not the language for you.
I agree with everything above that line and probably the intent of that line too.
However I'd phrase it as:
If you don't want to be forced to make a large effort managing memory for the given task, C is the wrong tool for that job. Even when you're great with C. Even when C is wonderful for a different job because you do it fabulously well.
I use C and like it. I don't use it when python is good enough for the job because in those situations python is likely to be actually better than C because you'd incur a debt to pay for something you don't need and as a result you just might miss one of those payments. The interest rate on a missed payment can get pretty steep.
Prefer shell, ruby, perl, something jvm, whatever? - Great. Exactly the same point applies.
I'm sure this point does /not/ apply to your comment above but it could be (mis)interepreted that way - C programming macho arrogance is not helpful for good quality software. C macho arrogance is pretty destructive, in fact and IMHO the worst thing about the use C (& C++ etc.)
C is definitely not the right tool for every job. And in fact, I don't write that much C for that reason, the cognitive load of tracking every bit of memory is high and not always justified.
I wouldn't put C and "modern" C++ in the same class though. They are actually very different from a memory management perspective.
You can write C++ like you write C, there are plenty of valid use cases for that. However, the "standard" way of doing C++ now implies things like containers, RAII and smart pointers. It means objects get allocated and de-allocated automatically, dangling pointers and buffer overflows are less likely, but it has a cost in terms of performance and control. Another tool for another job.
One of the things C and Modern idiomatic C++ (using C++11 on) have in common is some part of the culture surrounding them having a tolerance for, if not liking of, macho arrogance. "If you got burned it's purely because you're stupid and lazy which I am not, ha ha." This kind of vibe. Not everywhere by any means, but it exists and it's destructive to sensible analysis and the writing of good quality software, while alienating some talent from the pool of potential colleagues. I strongly dislike it.
> More like writing C properly.
A lot of C problems could have been solved within the language without adding any kind of advanced CompSci construct. C is fundamentally outdated as a language and rely on assumptions and restrictions from the 70s.
Unfortunately each time someone wants to replace C, they stuff the remplacements with the unnecessary evolved constructs I talked about.
A language shouldn't need to be "written properly". Manual memory management doesn't have to be hard at first place, it is made hard by C weakly typed nature and horrid syntax.
The worst example of undefined behaviour abuse by a compiler is when gcc developers decided it was a good idea to optimize away a NULL pointer check just because the pointer was dereferenced. Seriously? Sometimes I really wonder who comes up with the idea that removing code the developer wrote is OK, but I guess that's the primary goal of any optimizing compiler.
I wrote a fast Json parser a while ago and one of the largest optimizations was to look at the size of the file, compute the worst case of how much memory it would need, and then do one allocation and then use the memory.
Another example would be a dynamic list of values. If you use a linked list, and have 64bit pointer and a 64bit value, each link costs 16 bytes. If you have a list of say 8 values then that's 8 allocations and 16*8 = 128 bytes. If you instead allocate speculatively an array of 16 values also taking 128 bytes, and then only use 8 of the values, you only need one allocation. If the number of values you need grows beyond 16, you need to reallocate, and expensive operation, but compared to making one allocation each time a value is added its much faster. Also all values are close to each other in memory so cache coherence will be much better, and you may end up with an order of magnitude faster access. This can be true even if you do expensive operations like insert. Yes, you can improve the performance of a liked list by allocating lots of links in one allocation, but the added space of the pointers, and the out of order access will still cost you.
Obviously, when you do this you should provide functions that always allocate the right number for the user of the code so that there is no magic numbers visible to the end user....
It is not!
* If you know exact reasons and exact how much space need (over) allocate, then you should document the reason, and it is no long over allocating.
* If you don't know the exact reasons and only know once a while it requires additional space, then you are not solving the problem. You are kicking the can down the road, and it is likely to explode later, possibly with inappropriate timing.
* If you know exactly how much the space you need, then the over allocating is just misleading code.
Fix Your Code!
EDIT: Your comment apparently is for something else. Anyway, the headline needs some counter correction.
An advantage of using realloc, is that a smart memory allocate can sometimes get away with not moving the memory, either by extending the allocation if the memory beyond the allocation happens to be free, or by doing memory address remapping.
The amortized runtime for N push_back will only be O(N) if the underlying array is resized to a constant factor > 1 times the required size (e.g. doubling it each time it's full). When prefixing with reserve of the actual required size, then, as you say, the amortized runtime for the N push_backs will be O(N^2).
I once investing a particularly slow routine to import csv to numpy-array of a million lines or so. It read csv lines in a numpy array, calling hstack for each new line, resulting in huge runtimes. Since then I have seen similar misuses of hstack and the like.
When you do similar, it is important to resize (by hstack or whatever) to a constant factor > 1 times the required size. Don't forget to remember the current size, as the size of the underlying array is now the capacity.
So far I was thinking as well that any "character" could be saved ("encoded"?) as UTF-8 by using max 4 bytes... .
If you’re tokenizing UTF-8 strings or trying to count “displayed” characters, then you also have to watch out for zalgo text that can use unlimited chains of code points and combiners to construct a single phrase out of potentially tens or hundreds of code points. Think it as a recursion limit of sorts.
I feel that Unicode and cryptography share the aspect of “don’t try to write your own code to deal with it, because you’re certain to miss something important”.
Technically the only thing UTF-8 encodes is Unicode code points, as higher level abstractions like grapheme clusters are irrelevant for it. UTF-8 parser's job is to turn a sequence of octets to a sequence of code points.
So-called "user-perceived characters" approximated as grapheme clusters is a relatively recent innovation in Unicode terminology. Compare the modern version of the spec (https://www.unicode.org/reports/tr29/) since Unicode 5.1.0 (2008):
> It is important to recognize that what the user thinks of as a “character”—a basic unit of a writing system for a language—may not be just a single Unicode code point. Instead, that basic unit may be made up of multiple Unicode code points. To avoid ambiguity with the computer use of the term character, this is called a user-perceived character. For example, “G” + grave-accent is a user-perceived character: users think of it as a single character, yet is actually represented by two Unicode code points. These user-perceived characters are approximated by what is called a grapheme cluster, which can be determined programmatically.
With the previous one (https://www.unicode.org/reports/tr29/tr29-11.html) from Unicode 5.0.0 (2006):
> One or more Unicode characters may make up what the user thinks of as a character or basic unit of the language. To avoid ambiguity with the computer use of the term character, this is called a grapheme cluster. For example, “G” + acute-accent is a grapheme cluster: it is thought of as a single character by users, yet is actually represented by two Unicode code points.
So you can notice "the computer use of the term character" in both. Notice also that these grave and acute accents used as examples existed in pre-Unicode encodings too, and we still call them "combining characters".
Hence if we adhere to that old "computer use of the term character" then "UTF-8 characters" are Unicode code points, and "Unicode characters" don't exist; but if we use a modern colloquial meaning of characters as Unicode's "user-perceived characters" then "UTF-8 characters" don't exist.
- Is it robust
- Is it fast (enough)
- Is it the simplest way to achieve those levels of robustness and speed
If the answer to the first 2 is largely "yes" and the 3rd is "no" then I'd call it a bad hack. OTOH if the answer to 3 is also "yes" then it's a good hack. If the answer to 1 or 2 is "no" then it's not ready to ship.
In either case a comment saying "this is the simple/safe way, performance opportunity via..." Or "this is the complicated way because <reasons complicated way was needed, usually perf> and it this is why it works..." is good to leave in case performance requirements ever change or someone is trying to figure out why you went the way you did.
Apples and oranges. The network is unreliable and all code dealing with the network should treat it as such. The file system and disks are also unreliable, but are probably more reliable by a factor of 10K than the network. Engineers mostly choose to ignore these potential errors via unintentional decisions or do stuff like let the process crash because restarting an API server once a year doesn't matter. Of course, not everyone has that luxury. Determining how much memory to allocate ought to be perfectly deterministic which is why this is such a code smell.
> where is the line, exactly?
The line to me is pretty clear in this case! There are all sorts of reasons why this code would be acceptable: if fixing the code to prevent the bug[s] is sufficiently onerous as to cause more bugs than the fix would prevent, or is so much work that it would never be undertaken, or so complex that the code is un-mergable, an emergency hot fix... all acceptable so long as the code is documented as such. In other words, the line crossed here was in the comment. Where did the number 5 come from? Where's the link to an issue tracking memory allocation/estimation?
I believe most comments in code are worse than useless - that we shouldn't merely document bad code, but write code that is so blindingly obvious and idiomatic that comments detract from its perfection. Comments exist when we deviate from the platonic ideal — the real world with leaky abstractions, deadlines, bugs and dollars.
"[T]here are several references to previous flights; the acceptance and success of these flights are taken as evidence of safety. But erosion and blowby are not what the design expected. They are warnings that something is wrong. The equipment is not operating as expected, and therefore there is a danger that it can operate with even wider deviations in the unexpected and not thoroughly understood way. The fact that this danger did not lead to catastrophe before is no guarantee that it will not the next time, unless it is completely understood. (...) The origin and consequences of the erosion and blowby were not understood. Erosion and blowby did not occur equally on all flights or in all joints: sometimes there was more, sometimes less. Why not sometime, when whatever conditions determined it were right, wouldn't there be still more, leading to catastrophe?"
Networks are unreliable and 'have you tried turning them off and then back on' works well and we have extensive experience with them; adding some retries is well within predicted workarounds and tricks. However, parsing a data structure should be straightforward, exactly reproducible, simple, and always work and use the expected amount of memory; adding on arbitrary amounts of memory is not a standard workaround, and, somewhat like Mercury failing to be where Newton's theory predicted it should be, indicates that your mental model of the system is not merely a little fuzzy on the edges, but fundamentally incorrect and must be replaced by a completely different theory (like relativity), and in the true model, the safety and correctness may be arbitrarily different than what you thought they were (in the way that Newton & Einstein make arbitrarily different predictions if you go fast enough).
Understanding exactly what the cause of the problem. Hacks imply a fix that "seems" to work but not understanding.
Nice you were able to get constant memory. I was getting 40byte max memory usage difference between identical RPC calls on the esp32, but that seems due to Nim’s ARC garbage collector. Sometimes +20 bytes, sometimes -20 bytes but not leaking. Though maybe it’s an esp-idf thing. Took me a bit to just call it good and focus on more important things. ¯\_(ツ)_/¯
This happens in science too. Many constants are placeholders for stuff we do not understand.
In math (and informatics) you build your own universe with your set of rules. It means that you can and should understand everything (well, of course if you assume the hardware works perfectly as specified).
Physics is more mathematical than computer science, but there's no doubt that physics is a science.
I was surprised to see that, at least according to Wikipedia, opinions (of people of consequence) vary on this point. [0]
Seems to me perfectly clear that math is not a science, as it's not empirical, but this doesn't seem to be universally agreed upon.
[0] https://en.wikipedia.org/wiki/Mathematics#Mathematics_as_sci...
An important part of CS research is empirical. It's not because you have designed something yourself, that you don't need to observe it to understand it, or that you can answer any question about this thing analytically.
For example, the folks from high performance computing conduct loads of experiments to observe and understand which CPU design work best and why. Deep Learning people also run lots of experiments understand which model is easier to train and corroborate theoretical results from learning theory. etc.
As for _why_: knowing that you have a buffer of at least a certain size can allow you to avoid bounds checks, branches, and allocations. For hot loops this can result in a significant performance improvement.
I’m not making any claims for this particular case, but if a function taking a large buffer to modify needs some extra space, and the code can get faster by storing that at the end of the buffer, requiring the caller to reserve that space can mean not having to copy the buffer twice, increasing memory use and execution time.
A fairly common example is room for adding extra zero string terminators, so that an vectorized algorithm walking the input always hits one of them.
For example perhaps having a function or some other means to compute required buffer size, in a way that makes such padding requirements relatively opaque to the caller.
This isn't uncommon, and is why it's pretty important that performance-critical software also RTFM.
When I started almost four decades ago it was then true that bytes and characters were the same thing.
Never smaller as the minimum limits for char require at least 8 bits. But definitely can be wider than 8 bits, as is common on DSPs or some old mainframes. Though, even on systems that don't permit direct addressing of 8-bit char, C implementations will sometimes emulate it.
FWIW, POSIX mandates a fixed, 8-bit char.
Not entirely, there is a vague description of what a byte should be able to represent.
The C and C++ programming languages define byte as an "addressable unit of data storage large enough to hold any member of the basic character set of the execution environment"