Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I'm no C expert - too bad to hear that about his C code. However I can tell people here that Vector.Unboxed is a very common optimization as soon as you start thinking about performance in Haskell. Nothing "expertly" about it, really. I, for one, use it in all of my computational Haskell code.


That's actually the problem he's hinting at near the end as he's trying to make his point about optimizing C.

The optimizations he needs to make to the C code didn't seem clear to him when he was writing it, but they're very common optimizations for someone with more experience writing in C.

That he made the seemingly-natural optimization in Haskell while not affording C the same luxury is what's hurting his argument.


"Haskell is easier to optimize than C" != "Haskell is faster than C".


If Haskell is easier to optimize than C, then it could easily be that there's some amount of programmer effort, for which expending that much effort in Haskell yields a faster program than expending that much effort in C. If that amount of effort is in the range of effort most people are able to expend on a class of projects, then Haskell is faster than C for those projects. It may even be that those are most projects.

It is, of course, not the case that Haskell is faster than C with arbitrary effort expended tuning to the specific hardware - no one is claiming that.


Are you writing that in Haskell or in C? Because in Haskell, you'd want to use the /= operator. In C, you'd want to use strcmp.


I think you meant strncmp. Nobody ever wants strcmp, not if they know what it does.


We know what it does and it's the right thing to use. You must be thinking of strncpy or some other ennified string.h function.


You don't know what you're talking about.


It's also important to notice that Data.Vector.Unboxed has the same API as the other Data.Vector implementations and provides the same typechecking that the other implementations do. All it requires is that the underlying type have a Unbox instance. In most cases, you change:

   import Data.Vector as V
to

   import Data.Vector.Unboxed as V
and your code now gets the performance increase. (The reason it's not the default is because it's not most generic. What if you want to have a vector of IO computations? Good luck unboxing that. But if you just want integers, or something, that's much easier.)


If they have the same semantics why doesn't the library do an unboxing implementation for types that support it and not for the rest?


The semantics are different with regard to lazyness: a boxed vector can contain thunks (unevaluated values), so individual elements can be evaluated on demand, while an unboxed vector must be evaluated all at the same time (making it difficult to run a transformation in parallel with multiple threads, for example).


This sounds like exactly the sort of optimization that I'm always hearing a sufficiently smart compiler will make for me.


No, because compilers are prohibited from changing the meaning of the program. Changing a lazy value to a strict value is only permitted when the compiler can prove that the value will always be evaluated. It can't do that for unboxed arrays because that is a global change, so it has to leave it to the programmer.


Fair enough.

My complaint is that the C code isn't given the same chance. He even calls out that reading data with getc is a known performance problem, but then does it anyway. Any book on learning C will point out that getc is slow for reading lots of data, and fscanf or fread should be used instead.


From what I understood from his post, using a buffered input function would a) make the code differ from the specification, b) require more refactoring than the Haskell code needed. b) seems particularly important when the program is not <100LOC, but tens or hundreds of lines of code.


The problem is that his implementation is essentially a test of how he's using libc versus how Haskell is using it.

The pointer arithmetic he's using shouldn't need to be optimized, since the page sizes he's malloc'ing and pulling from memory are small enough that they should stay in the L1/L2 cache for the entire run(he's using 1k blocks of data, most processors use 4k pages). There's almost no optimization to be done there.

The biggest performance hit is actually the single character puts versus a block read or write.

Haskell is probably implemented to read a large block of the file(or perhaps the entire file) into memory, then parse after. That would be a minimal number of system fread calls over the entire run. Versus the C code, where 1 block's parse could be 1024 getc and putc calls.


It's not about system calls, it's about locks. The getc function is buffered (by default) - that's what's going on behind the scenes in that FILE structure. What is slow about calling getc over and over is synchronization around that FILE object (hence the existence of functions getc_unlocked, &c).


I would never encourage use of the f* I/O functions from the standard C library for high performance code. They're all buffered which means there's an extra copy happening. You should use read and other POSIX I/O functions instead.


This is not 100% true.

f* calls are buffered this is true. But depending on your workload this may be a blessing in disguise because if you're parsing a very long stream of unstructured data you'll have to re-implement most of that buffering logic in your own code to deal with the inevitable corner cases where your data overlaps the borders of the buffersize chosen. The obvious optimization here is to pre-allocate a buffer of the right size and read in the data in one go but for a stream of unknown and possibly infinite length (such as used in a filter like this) that is not possible.

So f* function have their place, even in optimized code, but should be used with care. If you're reading data with fixed size blocks (such as in this example) then yes, a simple 'read' call will likely be faster, because as you note the read call will place the data directly into a user accessible buffer without the need to go through more library calls to get at the data. (fread is a wrapper function around the read system call).


My impression is that, while it is possible to come up with a performance oriented Haskell code, it becomes quite ugly and diverges from those beautiful and idiomatic examples found in books. E.g. explicit strictness specifications, real quicksort (with Array.ST), etc..


The same applies to C, of course.


I wouldn't say so. Some books indeed skip some "real-world" details, but that doesn't affect idiomatic C, which is already performance friendly.




Consider applying for YC's Winter 2027 batch! Applications are open till November 2.

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

Search: