Hacker Newsnew | past | comments | ask | show | jobs | submit | psanchez's commentslogin

In the same line as therealmarv's comment, I think I prefer working with agents and multiple clones rather than with worktrees so far. Probably the main reason is that it has better isolation and I don't need to copy non-committed files (e.g config) over and over. I can just keep working with agents on the very same folders, even when I need a new feature.

I think there are some pros/cons to each approach and would depend on each person's individual workflow and project(s) more than anything.

In my case, I've been working on a monorepo the past weeks, and this is the setup that I've found useful:

  - project/
  - project2/
  - project3/
  - project4/
You can easily spawn (or even recreate) a new folder by doing:

  $ cp -r project project5
All my non-committed config files were copied from project/ to the other folders.

Before I had this setup, I was working with just 1 docker-compose.yml file (e.g. docker compose watch), however, when working with multiple folders and agents in the same computer at the same time, this approach stopped being useful.

Since my local setup for this project had a db+backend+frontend, I instructed AI to create a command to just launch variants of my local setup but using different ports to avoid conflicts with the other copies of my project.

  $ ./build.sh --watch 3
This number, I use it as a sort of prefix on the existing exposed ports in my local machine, to help me mentally map ports easily. For example, for 3:

  - 3080    (http)
  - 3443    (https)
  - 33306   (mysql)
  - ...
  - 35432   (postgres)
The reason I'm using it as a prefix is convenience, it's very easy to remember which port should I open for each of the clones.

I found using 3 to 4 parallel clones/AI workers to be my sweet spot in terms of being able to manage the development of different features in parallel and not lose track of what's where, while getting the most of my AI subscriptions. I usually keep some notes in a piece of paper like this:

  1) feature blah
  2) bugfixing of blah
  3) research blah
  4) bugfixing of blah blah
Then if I'm reviewing the work of AI on 4, I know I can just go to the browser to 4443 and do some manual testing if I need to (of course, there are unit / end2end tests, but that does not prevent me from doublechecking and ensuring what I had in my mind got translated in code to the actual thing I wanted)

When I finish with one of the features/bugfixes I'm working on, I push changes, pull master repo again, and start another branch & feature. I keep using that same folder and ports.

Another thing I found to be very useful is to set up these local servers with the fixtures I use for the end2end tests, and that way I have a common setup with the same data to do my manual tests. I only use one command to set everything up (the one above) since I don't want to remember nor type a bunch of commands every time.

Sometimes I don't even use clones/worktrees at all, but just work on multiple projects in parallel (again, 3 being the sweet spot, 4 is already challenging to keep track of what I'm doing, and I would probably only go 4 or 5 if I'm doing really long tasks from 1 to 3). BTW having fully autonomous agents working in parallel and doing PRs automatically is totally separate from this local workflow I'm describing, both things can coexist.

Just as a side-note, if agents were 10x-100x faster (at current Sol/Opus5 level) I would probably work only on a single repo/task at a time in my local computer. The reason I multitask today is just because waiting for the models takes minutes and not seconds.

Edit: minimal changes/formatting for clarity


Same here. I'm waiting for Cerebras to host gpt 5.6. I moved the daily driver to a Pi harness (www.freepi.ai) on DeepSeek V4 Flash from a fast provider. (disclosure this is my side project- free inference). I'm using it for implementation: Sol for planning, and then this for implementation.


This reminds me of a story from 15 years ago, where I was developing a technology to download games on demand by hooking into the OS calls.

There was a particular game that was superslow when this tech was applied. Original game loading took around 15-20 seconds, whereas once the tech was applied it took easily 3-5 min, even with all data already downloaded.

When I started digging into it, I realized the reason was the game was using something like

   fread(data, 1, 65536, fptr);
instead of

   fread(data, 65536, 1, fptr);
Which basically expanded back in the day to 65k reads of 1 byte for several MB file. Each fread translated to 65k reads of ReadFile Windows API. Since my code was hooking on ReadFile system call, and my call was heavier than ReadFile, the game loading felt really slow. Unusable. It would have not been fun for players.

The easy fix was to swap arguments for certain calls. The long fix required to use an internal cache to account for these cases so that the hooked ReadFile was faster when data was already in disk.

Funny thing is that as we started rolling out the tech and applying it to more and more games we realized lots of games did this. We went for the cache fix and games ended up loading faster than before. Honestly, games could have load all the data in a couple of seconds by just swapping the args. I'm guessing developers did this on purpose so that games seemed like they were loading a lot of stuff, although you never know.


I used to be a graphics card/chip architect for macs in the early/mid 90s - our chips were the fastest, but some programs were resistant because they did stupid stuff: pagemaker invalidated the font cache every time it went thru its main loop, quark with ATM did an n*2 thing every time it wrote text etc etc. We had special hardware to accelerate text drawing and it did nothing because the software pissed it away. We considered creating a plugin that fixed all these things, it would have been hard to maintain, in the end we travelled around to the people who made these apps and talked them through their problems

To be fair excel would erase places white that it wanted to write up to 9 times before it drew any black pixels, we made that very fast! we didn't tell them :-)

At the time 24-bit framebuffers were so slow that before we built graphics acceleration hardware people would switch back to 8-bit to get stuff done, making 24-bit/true colour your daily driver was a big step forward.


Does that make you the first in a long tradition of GPU developers going to blockbuster app devs to say "hey, you should be doing this instead?"

PS – I am looking through the NuBus cards that I have... did you work for SuperMac or RasterOps?


I was probably not the first to have to do that, we knew what apps our customers used, making them better was the whole point of the operation

I did the architectural design for the SuperMac cards. I figured out what needed to be accelerated, dropping code into people's machines to see where the cycles were going. Others did the physical design for the first 2 cards, I did the design of the chip in the Thunder and later cards (designed the data paths and state machines and a full simulation, someone else actually laid the gates)

If your card has a SQD01 on it it's my work. It peaks at 1.5Gb/s on solid fills


This is a horrible and yet not unexpected insight into the internals of Excel


To be fair this was Excell 25 years ago, may no longer be true.

One of the other bugs (the Quark/ATM one) was also because of the programmers were worried about writing over stuff that hadn't been completely erased, the Quark guys wrote a string with 2 spaces at the end through a box that masked the end of the string, the ATM font renderer saw it couldn't fit the text so it split it in half and tried again so it drew N/2 N/4 N/8 ... strings. It spent all it's time in the 68k's multiply instructions figuring out how wide the strings (and substrings) were, our fancy 24-bit character rendering hardware was an afterthought


Nowadays Excel is a webapp that reflows the entire DOM 400 times per keystroke!


There's a good chance it was Excel's workaround for some other GPU's buggy behavior.


there were no GPUs at that time. What we were building in the late 80s/early 90s were the first generation of Mac graphics accelerators, really just glorified blit engines, a class of things that eventually grew into what we now call GPUs as people started to push CPUs into them


I see. I took "early/mid 90s" literally and thought you mean like 1991 through 1995, inclusive.


In all of the software you’ve written, are you aware of how many on-screen pixels you’ve overdrawn?


> To be fair excel would erase places white that it wanted to write up to 9 times before it drew any black pixels

I feel like I'm having a stroke trying to read this, what does it mean??


Well all they needed to do was erase the screen with white and draw on it, but their app's internal logic meant that they erased it more than once.

I was capturing QuickDraw library calls - the low level graphics primitives, to figure out where the graphics time in apps was going and found out sometimes excel did it 9 times

Of course users didn't see it more than once, but our hardware made all that wasted time run faster


It's more likely that one dev wrote the draw-cell code.

Another dev who's fixing a bug, realizes if they call a certain function either directly or indirectly, their particular bug gets fixed.

Oh, and as a side effect, the cell gets erased (again).

A few more fixes/new features added like this and the code is inadvertently erasing the same cell multiple times.

It takes a certain type of dev to step through in a debugger and Notice the app is doing way too much work and then to untangle the mess of code without causing regressions.


Maybe their CRTs had horrible burn-in and they had to erase everything 9 times before it was gone...


Several layers of white is what makes the black really pop. (Just kidding).


It means they were time travellers! Secretly, they came from an alternate future where everyone used e-ink displays, and wanted Excel to be ready!


I think it could call (their equivalent of) clearRect up to 9 times on an already cleared region before drawing there?


It’s necessary for erasing cat pixels.


before writing to some area, it would erase it (clearing with white) up to 9 times


I remember when 24 bit color was exotic and aspirational and you had to settle for 16.


I swear if Sun Microsystems was still around, their machines would still ship with 8-bit pseudocolor and you'd have to pay an extra $3k for 24-bit.


I got the extra vram in my LC to allow for 24-bit color but it was dog slow. The 16 bit data path didn't help. If I wanted it, I'd get things done in 8 bit or mono until it was ready, then switch to 24 bit for the final look.


Yeah, even in Linux we were doing these things with X Windows bit depths.

8 bit psuedo color, so the color palette switched with every focus-follows-mouse window boundary crossing. 16 bit direct color with banding but no more palette psychedlia.

This was equal parts to make it faster and to allow for higher framebuffer resolutions with limited VRAM.


16 bits? Luxury. I had 6 colors when I was a kid and was happy to have them.


My first computer was a TRS-80 Color Computer which had a tiny set of badly chosen colors!

Back then you did what you could with graphics and it wasn't a lot. After I got a PC I had indexed color for a long time and working with indexed color was pretty rough because anything physics-based like rendering or raytracing was going to be difficult. You could render a photo pretty well with 256 carefully chosen colors and dithering but if you wanted to, say, composite two photos and do general sorts of things you'd need to convert to "true color", do the math there, then re-quantize for display.


6?!? We had only 4 colors in low res mode and 2 in high res


Well my Hercules graphics card was only monochrome, but it was relatively high resolution.


I had a herc clone on the 286 machine I bought around 1987 and later added a Super VGA card. One cool thing about the IBM PC was that the monochrome and color graphic systems were sufficiently different in terms of memory map and ports so you could plug in two graphics cards and two monitors and that's what I had.


I think this was true for MDA (text only), but the Hercules had 64k of video memory at B0000, the latter half of which would overlap with the CGA card, and also VGA's text mode.


In my 68K Mac emulator running on modern (or even decade-old) hardware, performance in the traditional sense is less of a concern, but other issues arise. The big ones include CPU-burning loops that wait for a length of time or for an interrupt-decremented counter to reach zero, as well as invalid memory accesses (which I've made crash — no NULL deref for you).

> We considered creating a plugin that fixed all these things, it would have been hard to maintain, in the end we travelled around to the people who made these apps and talked them through their problems

Since talking to developers is no longer an option, I actually do write "Such-and-such Tune-up" extensions that patch applications dynamically to make them run better (or at all) in Advanced Mac Substitute, or even Mac OS itself.


Yeah those low core global system variables (including a readable/writeable 0) at fixed addresses were very much a thing, they were a bad design decision made for the original Macs with almost no memory, and made running more than one app (switcher/multifinder) a difficult transition back in the day. Someone wasn't planning ahead

I also worked on the original A/UX port for the Mac II, some hardware (like the IWM) required tiny buzzy loops, we ran into one bug where using the floppy caused ADB to freeze, but only on the release machines, not the prototypes all our engineers had, turned out there was hardware that made access to the VIA faster by pulling the clock in for 1 cycle, if you sat in a loop reading the timer in the via to measure a sector time for the IWM in too tight a loop it upped the output clock from the VIA to the ADB chip and over clocked it ....


What would have been the purpose of stupid code like that?

Was it a workaround for things that didn’t fully complete on one iteration, so the devs kept hammering away at it until it worked?


They were most likely just bugs. Quite possibly really stupid bugs.

Not every bug results in the program doing the wrong thing, they often just make the program do the right thing very slowly.

And nobody notices, since it still produces the right result.


Yes, they were bugs, I think programmers (and their marketing people) were more focused on new features than performance


Thankfully we’ve moved past that era.

Now the bugs that get ignored for new features cause bad results AND bad performance.


It's not necessarily stupid code in the game, but something the C library is doing that it probably shouldn't.

If the stream is buffered, then all operations, including fread, are supposed to go through the buffer.

All three of these should issue buffer-sized reads to the operating system:

1. A loop which calls getc(stream) 65536 times.

2. fread(buf, 1, 65536, stream)

3. fread(buf, 65536, 1, stream)

The more direct behavior of fread should only kick in if the stream is configured as unbuffered.

I would say that the way low-level reads are issued to the host operating system is a "visible effect" of the program, so I suspect this may actually be a matter of conformance. I.e. it's not okay to issue those reads however the stream library wants as long as the data is read.


Reminds me of the "community patch" to GTA Online from a few years ago. The game was plagued by 10+ minute loading times. The situation remained for years and only got worse with time. Some hacker figured out that the game spent 80% of loading time reading the in-game store listing file. The file was tens of megabytes IIRC, and it literally used the Schlemiel the Painter's Algorithm - for each entry, start reading from the beginning byte after byte. The hacker made a tiny patch that made it remember where it found the last entry. This cut the total loading time by 80%, from over 10 minutes to less than 3.

Edit: removed incorrect information.


This is not quite an accurate telling of rockstar's reaction, there were actually receptive to it and paid out $10k for the discovery. Though it's an understandable mistake given rockstar's hostile history with the gta modding scene.

See the original post and discussion for the whole story:

https://nee.lv/2021/02/28/How-I-cut-GTA-Online-loading-times... https://news.ycombinator.com/item?id=26296339


That's not how I remember these events when they were playing out. I distincly remember social media posts warning about the dangers of modifying game files, plus refusal to acknowledge the issue. Note there were 2 full weeks between the blog post and the update mentioning the bounty. I'm pretty sure the massive community outrage in between has played a role in it. But I don't have any sources and I was wrong about at least one thing (lack of attribution), so I'm okay assuming I'm wrong about everything else too.


Wowee two full weeks? You mean like a single sprint to discover, verify, and post PR about a perf patch that was good among the sea of rumors and reports a billion dollar game usually gets?


I mean like enough time to check the pulse with the community and walk back the initial confrontational response. I don't have a problem with when they fixed it. I don't have a problem with when they paid out. I wouldn't have a problem if they didn't pay out at all (why would they?). I have a problem with their initial reaction, which was full of the usual fearmongering against modders. (And a smaller problem with that it took an external contributor to finally make them implement a trivial fix for a massive usability issue that's been there for at least 6 years. It shows how much they don't care about their customers or the product they're selling unless the media get involved.)


> Which basically expanded back in the day to 65k reads of 1 byte for several MB file. Each fread translated to 65k reads of ReadFile Windows API

What software did that that badly? If the code asks for (up to) 65,536 single byte items, why would you split that into 65,536 calls?

Also, that change changes behavior. The old call could read anything from zero to 65,536 bytes, the new one only can read zero or 65,536 bytes.

(Reading the source of a few implementations, I think most implementations will fill the output buffer with partial objects if the input doesn’t supply an integral number of them, but the return value of fread cannot signal that to the caller)


The standard says that fread calls fgetc multiple times for each object:

> For each object, size calls are made to the fgetc function and the results stored, in the order read, in an array of unsigned char exactly overlaying the object

(wording unchanged since C99)

If the file is unbuffered, depending on how the implementation handles buffering, and how it interprets the standard, then perhaps it does end up hitting a path where there's 1 ReadFile call per byte...

I don't know how most implementations get around this. Presumably it's valid to interpret "calls are made" as "behaving as if calls are made", meaning fread can copy data out of the FILE's buffer directly, or make calls directly to whatever routine fgetc defers to, rather than calling fgetc N times literally. Looks like glibc's fread does this.


> The standard says that fread calls fgetc multiple times for each object:

>> For each object, size calls are made to the fgetc function and the results stored, in the order read, in an array of unsigned char exactly overlaying the object

Aha! That phrase led me to https://man7.org/linux/man-pages/man3/fread.3p.html. I consulted https://man7.org/linux/man-pages/man3/fread.3.html and https://man.openbsd.org/fread.3. Neither mentions that.

Now, I checked https://cplusplus.com/reference/cstdio/fread/. It doesn’t mention it, either.

⇒ this appears to be POSIX-specific.

Finally, if somebody implements fread as “For each object, size calls are made to the fgetc function”, it doesn’t matter whether you ask for 1 object of size 65,536 or 65,536 objects of size 1; both would call fgetc 65,536 times.


Shouldn't you be using cppreference.com instead of cplusplus.com? Because the former [0] actually has this language:

    Reads up to "count" objects into the array "buffer" from the given input stream "stream"
    as if by calling fgetc "size" times for each object, and storing the results, in the order
    obtained, into the successive positions of buffer, which is reinterpreted as an array of
    "unsigned char".
This whole fread/fwrite's interface is hailing from the time where some OSes used record-based filesystems and were literally unable to read/write less than a record at a time.

[0] https://en.cppreference.com/c/io/fread


Maybe, but that text says “as if” and even “If an error occurs, the resulting value of the file position indicator for the stream is indeterminate. If a partial element is read, its value is indeterminate.” so it doesn’t in any way require implementations to implement it by actually reading one byte at a time.

That description also fits what I saw in the implementations I inspected, both of which simply try to read size × count bytes.


By the way, behold the original (from UNIX Version 7) stdio package [0]:

    fread(ptr, size, count, iop)
    unsigned size, count;
    register char *ptr;
    register FILE *iop;
    {
        register c;
        unsigned ndone, s;
    
        ndone = 0;
        if (size)
        for (; ndone<count; ndone++) {
            s = size;
            do {
                if ((c = getc(iop)) >= 0)
                    *ptr++ = c;
                else
                    return(ndone);
            } while (--s);
        }
        return(ndone);
    }
Thankfully, the definition of getc() [1] is indeed

    #define getc(p)  (--(p)->_cnt>=0? *(p)->_ptr++&0377:_filbuf(p))
Interestingly enough, because there is no explicit multiplication, this loop properly works on systems with e.g. 16-bit unsigned int but 32-bit pointers (and overflows just the same on systems where ints and pointers are the same size).

[0] https://github.com/v7unix/v7unix/blob/master/v7/usr/src/libc...

[1] https://github.com/v7unix/v7unix/blob/master/v7/usr/include/...


I think it’s pretty rare for files to be unbuffered like that. AFAIK it’s mostly stderr that ends up unbuffered, at least on Unix-like systems.


You can call setbuf(fp,NULL) after opening, and now the stream is unbuffered. What this means is apparently implementation-dependent.

As to why you'd do that? - well, who knows the exact circumstances in this case. Perhaps this was faster in some meaningful case that was relevant to some other project (and then maybe the fread doesn't call fgetc after all!). I'm just speculating. Well-reused code often ends up with stuff that needs rethinking, that, even if noticed, nobody has the time or inclination to attempt to fix.


A long time ago I worked with someone who read 1 byte at a time from a socket because they insisted data was cached so the kernel was going to batch it magically somehow. It took me days to convince them to measure it.


I used to make it a general rule to start all my optimisation of any network code by running strace and look for excessive read's and write's, because you'd be shocked how many did stuff like that if they didn't know the length of a string, or to read the length first, instead of reading into a buffer.

I had to convince people with benchmarks regularly that, yes, you could write the handful of lines to do proper user-space buffering and trivially run rings around any code that did extra context switches, because a lot of people didn't realise the cost difference between system calls and calling their own functions.

This included, by the way, the MySQL client library, at one point, which would do small read for length fields instead of larger non-blocking reads into a buffer all the time


That's different: you're talking about the application code, like OP.

But I think the parent comment's point is that the issue is in the implementation of fread itself in the standard library. It's perfectly reasonable for an application to pass it 1, 65536 (i.e. one byte, up to 65536 times) and expect it not to issue 65536 separate OS calls.


Is it? I get what you're saying, but asking for 1 byte 65536 times, is indeed different than asking for 65536 bytes, 1 time. There may be reasons, such as when you pull off the end of a buffer, it shifts. And the buffer size is 1 byte. Or 10. Or whatever.

No, I'm not saying that's why. I'm simply saying there is a difference between asking for 1 byte or 65k bytes of something. Even dd runs the same under Linux.

dd bs=10k count=1 is faster than bs=1 count=10k

I remember trying to recover some data from a spinning disk, and trying to slowly creep up on the data. So I wanted 1 byte per, I wanted it to nibble, until it hit whatever the errored part was. If I just grabbed the lot, it'd error out from the whole read.


I glanced at https://github.com/busterb/libc-openbsd/blob/master/stdio/fr... and https://chromium.googlesource.com/chromiumos/third_party/gli....

The latter (as usual when comparing OpenBSD and Linux) is more complex, but both multiply count by size and then go their way.

Also, the API contract allows fread to read fewer bytes than requested. I would except any implementation to do that.

But maybe, somebody interpreted the contract differently than major OSes, in the sense that a call isn’t allowed to write partial size-sized chunks to user memory and/or advance the file position further than its return value advocates (that, I think, is something that the implementations above can do, and might be considered a bug)


> asking for 1 byte 65536 times, is indeed different than asking for 65536 bytes, 1 time.

Yes it's different. As others have noted, the difference is what is returned if less than 65536 are available to read in the file: total failure vs partial read.

There is, unsurprisingly, no requirement that it has an unnecessarily inefficient implementation to meet this behavioral requirement. (The C standard doesn't talk about such things as syscalls but, even if it did, it surely wouldn't require such a thing.)

The irony is that that partial read is actually the default on both Windows and Posix (i.e. both ReadFile and read() will read up to the number of bytes specified). So a one-syscall implementation for fread would have been easier than multiple calls, and certainly would be standard compliant.

The dd example isn't comparable because dd is much lower level, and you really are specifying how the syscalls should be made.


Also you need to be careful what you read/write. In some cases.

As many examples out there use int/char etc to show how to use the thing. But if you switch to structs that fwrite can totally burn you if you use the sizeof call. As the sizeof a struct can vary between platforms and compilers. Depending on packing. Then endianness can sometimes mess you up. If you are reading/writing for yourself you can get away with a lot. But if you are trying to interop then you have to be wildly careful what you do.

fwrite is another one where people will do one byte at a time (same up to for the windows version). Bash out a loop, use the sizeof for the input to the for loop. copy and paste just doing 1 byte and you can easily end up here. One program I added a cache in front of the thing so it would always write on disk block boundaries and then come back for more. I started off with just packed struct sizes but the perf was just 'ok'. The file block boundary thing really made it fast. Not all OS's have a readahead/write buffer behind that call so perf can vary.

It is honestly such an easy mistake to make. As many of the examples/docs do not really show you why/how to use both of those calls in the way needed. You sort of have to stumble into it and work it out.

Once you see it you know. But until then you do not really notice if it is 'working'.


Are you sure you're not thinking of raw read() / write() (the operating systems calls) rather than fread() and fwrite() (the C standard library functions)? fread and fwrite already have a cache, so repeated calls won't make lots of OS calls.

Or, at least, they shouldn't! Clearly OP's implementation was a counterexample (it made lots of OS calls just for a single fread call).


Unfortunately not all of them. It depends on the device you are using, and the storage device, and the kernel, and the CRT you are using. The process I was using went from like 20-30 seconds to run to less than a second once I put a cache in front of it. Current implementations may have that cache. I would look first and see what they are doing. At the time I was working with about 6 different CRT's of varying age and quality. Some systems you can turn it on and off per device, or global, or per call depending on flags passed in. Really it comes down to reading the docs (probably right), and testing it out (empirical evnidence). That project I learned to not trust the c runtime libraries. But to verify what they are doing and re-read the docs and make sure I am even using the thing correctly vs what is written in the code and my assumptions.


Another possibility for why it needs to be done that way is dealing with error conditions.

I've not looked at the code (or even the man pages) and it is a long time since I touched anything that low level, so this might be completely wrong, but if there is an error before the next 64KiB (including just hitting EOF) then the semantics could be different. Asking for 1x64KiB I would expect to just error as there aren't the requested number of bytes. Asking for 64Ki lots of 1 byte might simple error just the same, or it might at least populate the buffer with what it can read, or if the meaning of 1,65536 is actually “up to 64Ki lots of 1B” then it would populate the buffer as far as possible and return the amount read rather than an error condition.

If the per-byte option is slow but still fast enough, and dealing with the semantics is less faf, then people will go for that because the tiny time loss is worth the larger effort reduction. Of course this assumes the underlying system doesn't change, as with the “making local code to run as on-demand networked code” example higher in the thread which changes the relative performance characteristics of the two calling methods significantly.


dd is designed to request a certain block size from the kernel. fread is not and should just multiply the two arguments and read that many bytes, just like calloc.


I assumed it was a simple mistake: easy to forget what order the two integers are sent.


Wait, is that wrong? I always call fread as:

    fread(data, 1, sizeof(buffer), f);
with the rationale that I'm interested in reading sizeof(buffer) individual bytes. The buffer size is incidental, not the size of the items I'm trying to read from the file; "read one item whose size is sizeof(buffer)" seems semantically wrong.

Is this just the case of Windows having a bad stdlib fread implementation 15 years ago or is my thinking here actually wrong?


It's not wrong. Guy just wrote a bad implementation of fread and blamed everyone else.


He didn't write it.

The C runtime authors did (presumably Microsoft, if it's MSVCRT).

He's hooking into ReadFile, a layer below the stdlib. By the time it reaches the hook, it's already split.


fread should be buffered, but different values may cause buffering at different rates. Perhaps it didn't generate 65535 calls to ReadFile but it generated 16 or 64.


Part of Windows Explorer actually does tons of tiny 4 byte ReadFile calls in to its tracking database like file when you delete a file. If you deleting lots of files this quickly adds up.


Is this why Windows takes so long to delete things?? Presumably those reads aren't done when using del from a console as that always seems a bit faster.


Its slowness is also a function of security software or any other file system "filters" (I believe they're called) are installed.

For example, I run TortoiseGit which has a caching feature which is supposed to make it faster at showing what to commit. Disabling it increases the number of items I can delete per second in my Windows Explorer from about 1000 to about 3000 while making not making TortoiseGit operations meaningfully slower (that I can tell).

This is a Dev Drive [0] on my machine, it would probably be slower on my C: drive which has full Windows Defender real time file scanning.

[0]: https://learn.microsoft.com/windows/dev-drive/


`rmdir /s /q` in a command prompt is significantly faster than Windows Explorer.

Yes C: is slow due to filters and Dev Drive is faster; but this difference can only be felt when using the command line; Windows Explorer has so much additional overhead that the overhead from file filters is insignificant in comparison.


Sure, my point wasn't that Windows Explorer is faster or slower compared to the command line or other OSes, but that deletion can be faster or slower (for Windows Explorer) depending on other factors too, like as per my example where it speeds up by 200% when I turn off that TortoiseGit option, which is nothing to scoff at.


>slowness is also a function of security software or any other file system "filters"

nah, its equally slow on system with everything ripped out (defender, filters, even logging).


Your dismissive tone implies you think I’m lying or something about my stated (and reproduceable) experience that deletions absolutely run a 1/3rd slower with certain software installed.


You gave an example of a third party program (TortoiseGit) slowing down deletion and implied Defender would also make it slower.

Im saying I have Win10 with everything ripped out, no defender, no logging, no tracelogging, no telemetry, zero filesystem filters/hooks. Delete speed is in the same ballpark as on freshly installed system.


Windows Explorers zip implementation also seams to do 1 byte reads by the speed is has compared to every other zip implementation.


It is frustrating how slow .zip (and more recently .7z) support built into Windows Explorer is.

This is a great article on why it's so unreasonably slow to modify these archives: https://textslashplain.com/2021/06/02/leaky-abstractions/

But it doesn't seem to explain why it's so much slower at regular extraction.


Doesn't that break anything relying on the return value? fread gives you the number of objects read as a return. So I think a pretty typical thing would be to fread and then parse that number of characters, and that'd just break?


I've seen a lot of code that just assumes fread / fwrite succeeded without bothering to check the return value...

But in this case if the code was calling fread 65536 times in a loop and getting 64KiB each time it wouldn't be good either!

Sounds like the parent comment had to fix this with the internal cache thing to speed up the small freads. I think they meant the easy fix would have been swapping the args in the original / caller code.


There are no small freads in the story, whatever implements those freads supposedly split them up into many calls. But that sound more like a problem of that implementation than the fread callers as size == 1 is correct when you are reading a bag of bytes.


Ah you're right, I misread it.


I think they turned it from a tiny file read to a tiny ram read.


The type of programmer who swaps the args to fread tends to be the type of programmer who doesn't bother to check the return value, fortunately.

Edit: mort96: So did you check the return value or not?


If I have a buffer of bytes, and I intend to treat the content of that buffer as individual bytes, what is semantically wrong with "read 65k 1-byte-sized items into this buffer"? Wouldn't it be a bit unnatural to express it as "read one item whose size is 65k"?


But the args aren't necessarily swapped just because they end up in a slow case in some implementation.


"I'm guessing developers did this on purpose so that games seemed like they were loading a lot of stuff"

I really hope that was not the case and rather think incompetence or to deal with obscure legacy problems, but the gamer in me gets enraged at the thought someone would artificially increase loading times.


Is this actually real? I thought fread just multiplied the two numbers together to compute a total size. Meanwhile, the Win32 API call ReadFile actually does do a separate system call if you call it multiple times.


> The long fix required to use an internal cache to account for these cases

That's because the OS does the same thing too. It's the right fix, when I implemented something similar, we implemented caching right away.


Why does your fread to anything other than multiplying the two arguments?


The idea of having two arguments to fread() is presumably to be able to do something else than all-or-nothing when there's a short read.


Yes, it divides the bytes read by the element size to get the return value.

Which is the obvious reason you'd pass an element size of 1: you want to know how many bytes were read.


The most important fix in SP1 for Office 2007 was fixing exactly that in Excel. Doing ridiculous amount of 4 byte reads made it basically unusable on network filesystems.


I love the fractal nature of it.


Even though I did not know about Andrej Karpathy's tweet from earlier this month, I ended up converging on something very similar.

A couple of weeks ago I built a git-based knowledge base designed to run agents prompts on top of it.

I connected our company's ticketing system, wiki, GitHub, jenkins, etc, and spent several hours effectively "onboarding" the AI (I used Claude Opus 4.6). I explained where to find company policies, how developers work, how the build system operates, and how different projects relate to each other.

In practice, I treated it like onboarding a new engineer: I fed it a lot of context and had it organize everything into AI-friendly documentation (including an AGENTS.md). I barely wrote anything myself, mostly I just instructed the AI to write and update the files, while I guided the overall structure and refactored as needed.

The result was a git-based knowledge base that agents could operate on directly. Since the agent had access to multiple parts of the company, I could give high-level prompts like: investigate this bug (with not much context), produce a root cause analysis, open a ticket, fix it, and verify a build on Jenkins. I did not even need to have the repos locally, the AI would figure it out, clone them, analyze, create branches using our company policy, etc...

For me, this ended up working as a multi-project coordination layer across the company, and it worked much better than I expected.

It wasn't all smooth, though. When the AI failed at a task, I had to step in, provide more context, and let it update the documentation itself. But through incremental iterations, each failure improved the system, and its capabilities compounded very quickly.


how is this related to parent comment . slop.


Well, my comment was meant as an example of a setup for actually building something real with reasonable quality. I was answering to that part of the previous comment.

In my experience, the difference is context. Agents without structure produce slop, but with a well-curated knowledge base and iteration, they can be useful. I was just sharing a setup that has been working for me lately.

Edit: minimal changes for clarity


I just had a look at the code and it is indeed very compact. I haven't compiled or used it.

Looks like RISC-V 32-bit integer and multiply and atomic instr extension. Floating point supported when compiling via gcc or similar the example apps (not by the emulator itself but by the compiler emiting the required software functions to emulate the floating point operations instead).

I think it is very clever. Very compact instruction set, with the advantage of being supported by several compilers.

Wrapper over this other project which is the one implementing the instruction set itself: https://github.com/cnlohr/mini-rv32ima

Kudos to both projects.


Wow. Really cool. I wasn't expecting something so polished.

JIRA speed drives me crazy sometimes, so a couple of months ago I decided to build myself a tool to do instant searches/filters on multiple projects right from the browser just to scratch my own itch.

I just wanted to see if I could have near-instant filtering. I think I got a pretty decent performance by using some JS tricks. I'm sure there might be ways to make it even faster.

Page is around 70kb (HTML+CSS+JS). Everything is manually crafted. I know the design won't win a beauty contest, but it does feel instant and works for my personal use-case. I had a lot of fun building this side-project.

There is a public URL, feel free to try it out [1]. Already mentioned in a previous comment in HN a while ago [2].

[1] https://jetboard.pausanchez.com [2] https://news.ycombinator.com/item?id=44740472

For the record, it uses a proxy because of CORS. Proxy is in few lines of golang. No NPM or any other framework used to make the project. In any case, if anybody is interested in the source code to run it yourself I'm happy to make the project public. Trusting a proxy on some random's guy on internet is probably a bad idea, given all NPM shit that happened yesterday, in any case, if you want to try, feel free, but use at your own risk :P



Looks cool, but definitely a security team's nightmare. Putting an API key into some random HN'ers hobby project is a bad, bad idea, whoever you are (not saying you're a bad actor, but a zero-trust policy would agree with me).


Indeed. That's why I was transparent from the start. As I mentioned, using an API key this way is generally a bad idea. Even if I'm not a bad actor (which I'm not, but you shouldn't trust me), if someone compromises my server and forges requests, they could potentially access your projects.

JIRA's OAuth implementation requires apps to be registered, involves public/private key pairs, and changes the auth flow. That adds complexity and makes setup harder, which is why I opted for a simpler API key setup, you get the API key, you write it down, you can make requests. It is just simpler and does not require JIRA admin rights.

For comparison, JiraTUI also uses the user's API token. The difference, I guess, is that it runs locally on your machine, but they could also send it somewhere else. At the end of the day, it comes down to whether you trust what you're downloading versus trusting what runs on a remote server. It is true that locally you could potentially inspect all HTTPS or even TCP requests whereas in the remote server you don't have a clue.

The thing is, OAuth in JIRA demands app registration and certificate management, so I guess many developers end up defaulting to user API keys as the path of least resistance, even if they encourage OAuth as well.


BTW, just to make it clear, in the case of jiratui you can also download from github repo directly and inspect the code if you wish :D


> they could also send it somewhere

Run JiraTui in a container / bubblewrap, and only allow it to connect to the Jira API host:port.


Wouldn’t that mean they could still exfiltrate it to another jira site they control?


Yeah, oauth would be better.


Fast is a distinctive feature.

For what is worth I built myself a custom jira board last month, so I could instantly search, filter and group tickets (by title, status, assignee, version, ...)

Motivation: Running queries and finding tickets on JIRA kills me sometimes.

The board is not perfect, but works fast and I made it superlightweight. In case anybody wants to give it a try:

https://jetboard.pausanchez.com/

Don't dare to try on mobile, use desktop. Unfortunately it uses a proxy and requires an API key, but doesn't store anything in backend (just proxies the request because of CORS). Maybe there is an API or a way to query jira cloud instance directly from browser, I just tried first approach and moved on. It even crossed my mind to add it somehow to Jira marketplace...

Anyway, caches stuff locally and refreshes often. Filtering uses several tricks to feel instant.

UI can be improved, but uses a minimalistic interface on purpose, like HN.

If anybody tries it, I'll be glad to hear your thoughts.


The jump table is interesting, although I guess the performance of switch will be similar if properly optimized with the compiler, but would not be able to tell without trying. Also different compilers might take different approaches.

A few months ago I built a toy boolean expression parser as a weekend project. The main goal was simple: evaluate an expression and return true or false. It supported basic types like int, float, string, arrays, variables, and even custom operators.

The syntax and grammar were intentionally kept simple. I wanted the whole implementation to be self-contained and compact, something that could live in just a .h and .cc file. Single pass for lexing, parsing, and evaluation.

After having the first version working, I kind of challenged myself to make it faster and tried many things.

Once the first version was functional, I challenged myself to optimize it for speed. Here are some of the performance-related tricks I remember using:

  - No string allocations: used the input *str directly, relying on pointer manipulation instead of allocating memory for substrings.
  - Stateful parsing: maintained a parsing state structure passed by reference to avoid unnecessary copies or allocations.
  - Minimized allocations: tried to avoid heap allocations wherever possible. Some were unavoidable during evaluation, but I kept them to a minimum.
  - Branch prediction-friendly design: used lookup tables to assist with token identification (mapping the first character to token type and validating identifier characters).
  - Inline literal parsing: converted integer and float literals to their native values directly during lexing instead of deferring conversion to a later phase.
I think all the tricks are mentioned in the article already.

For what is worth, here is the project:

  https://github.com/pausan/tinyrulechecker
I used this expression to assess the performance on an Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz (launched Q3 2018):

  myfloat.eq(1.9999999) || myint.eq(32)

I know it is a simple expression and likely a larger expression would perform worse due to variables lookups, ... I could get a speed of 287MB/s or 142ns per evaluation (7M evaluations per second). I was gladly surprised to reach those speeds given that 1 evaluation is a full cycle of lexing, parsing and evaluating the expression itself.

The next step I thought was also to use SIMD for tokenizing, but not sure it would have helped a lot on the overall expression evaluation times, I seem to recall most of the time was spent on the parser or evaluation phases anyway, not the lexer.

It was a fun project.


It looks like it is using "Microsoft Console Debugger (CDB)" as the interface to windbg.

Just had a quick look at the code: https://github.com/svnscha/mcp-windbg/blob/main/src/mcp_serv...

I might be wrong, but at first glance I don't think it is only using those 4 commands. It might be using them internally to get context to pass to the AI agent, but it looks like it exposes:

    - open_windbg_dump
    - run_windbg_cmd
    - close_windbg_dump
    - list_windbg_dumps
The most interesting one is "run_windbg_cmd" because it might allow the MCP server to send whatever the AI agent wants. E.g:

    elif name == "run_windbg_cmd":
        args = RunWindbgCmdParams(**arguments)
        session = get_or_create_session(
            args.dump_path, cdb_path, symbols_path, timeout, verbose
        )
        output = session.send_command(args.command)
        return [TextContent(
            type="text",
            text=f"Command: {args.command}\n\nOutput:\n```\n" + "\n".join(output) + "\n```"
        )]

(edit: formatting)


Yes, that's exactly the point. LLMs "know" about WinDBG and its commands. So if you ask to switch the stack frame, inspect structs, memory or heap - it will do so and give contextual answers. Trivial crashes are almost analyzed fully autonomous whereas for challenging ones you can get quite a cool assistant on your side, helping you to analyze data, patterns, structs - you name it.


I think I would call it naive algorithm rather than greedy.

It looked like an interesting problem so I spent some time this morning exploring if there would be any performance improvement by pregenerating an array of X items (where X around 1M to 16M items) and then randomly returning one of them at a time. I explored the project and copied the functions to be as faithful as the original implementation as possible.

Generating 10M unit sphere (best of 3 runs, g++ 13, linux, Intel i7-8565U, one core for tests):

  - naive/rejection: ~574ms
  - analytical: ~1122ms
  - pregen 1M elements: ~96ms
That's almost 6x faster than rejection method. Setup of the 1M elements is done once and does not count on the metrics. Using double type, using float yields around 4x improvements.

After looking at those results I decided to try on the project itself, so I downloaded, compiled and applied similar optimizations in the project, only updating circle and sphere random generators (with 16M unit vectors that are only created once on app lifetime) but got almost no noticeable benefits (marginal at most). Hard to tell because of the random nature of the raytracing implementation. On the bright side the image quality was on par. Honestly I was afraid this method would generate poor visuals.

Just for the record, I'm talking about something as simple as:

  std::vector<Vec3> g_unitSpherePregen;
  uint32_t g_unitSpherePregenIndex = 0;

  void setupUnitSpherePregen(uint32_t nElements) {
    g_unitSpherePregen.resize(nElements);
    for (auto i = 0; i < nElements; i++) {
      g_unitSpherePregen[i] = unitSphereNaive();  // call the original naive or analytical method
    }
  }

  Vec3 unitSpherePregen() {
    g_unitSpherePregenIndex = (g_unitSpherePregenIndex + 1) % g_unitSpherePregen.size();
    return g_unitSpherePregen[g_unitSpherePregenIndex];
  }
 
I tried as well using a psrng (std::mt19937 and xorshf96) in unitSpherePregen instead of the incremented variable, but increment was faster and yielded good visual results.

Next step would be profiling, but I don't think I will invest more time on this.

Edit: fix formatting


I also came up with an alternative implementation: https://gist.github.com/camel-cdr/d16fd2be1fd7b71622649e6bc7...

The idea is based on the Ziggurat Method. You overlap the circle with n boxes that each encapsulate the same amount of area of the underlying circle, select a random box, and then do rejection.

With 128 boxes, this reduces the average number of additional iterations from 27% to 0.7%, which should massively reduce the number of branch miss predictions.

It ended up about 2x faster the simple rejection method.

I haven't applied this to spheres yet, but that should also be possible.


Didn't know about Ziggurat algorithm, the use of a table to directly accept or reject is interesting, although I think I would need to implement myself to fully understand. Your comments in the code are great, but I still need I would need to dedicate some time to fully grasp it.

I'm wondering what if a 2D or 3D array was used instead, so that instead of working with the unit circle / unit sphere, you worked on a 256x circle/sphere.

Assuming the center of the circle/sphere was on the position (127, 127) or (127, 127, 127), then you could precompute which of those elements in the array would be part of that 256 sphere/circle radius and only the elements in the boundary of the circle/sphere would need to be marked as special. You would only need 3 values (2 bits per item).

   0 = not in the circle/sphere
   1 = in the circle/sphere
   2 = might be in or out
Then you would only need to randomly pick a point and just a lookup to evaluate whether is on the 2d/3d array. Most of the times simple math would be involved and simple accept/reject would cause it to return a value. I guess it would also produce the number of additional retries to 0.7% on a circle (one circle intersection for every 128 tiems = 1/128 = 0.78%).

From my limited understanding, what I'm saying looks like a simpler implementation but would require more memory and in the end would have the same runtime performance as yours (assuming memory and processor caches were the same, which are probably not). Uhm... I guess the implementation you present is actually doing something similar but with a quarter of the circle, so you need less memory.

Interesting, thanks for sharing.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: