Pruning spaces from strings quickly on ARM processors
lemire.me
lemire.me
I would suggest that the best way to generally hunt down a character class - i.e. white space is itself an 'algorithmic' problem and a mildly interesting one. If you have a fast permute handy this can be done on both x86 and ARM.
We have some code here that does this, but it's not the easiest to understand (this is the run-time only):
https://github.com/01org/hyperscan/blob/master/src/nfa/shuft...
The idea is that you represent a character class by intersecting two PSHUFB (or whatever floats your boat) results. So if you want to do a real regex-style \s, you will probably have to use one "bucket" to represent the low-order stuff (like \r and \n) and one for \x20. You then use 1 PSHUFB to look up your bucket for the high 4 bits (aka nibble) and the other PSHUFB to look up the low 4 bits.
This can't represent all character classes - it's possible to run out of buckets. There's an obvious way of doing all character classes with 3 shuffles (see truffle.c in the same directory).
As another commenter says, it's a lot harder to do this for UTF-8 as you are looking for bigger data items, variable-sized data items, and potentially a more generous definition of whitespace.
You will need more shuffles and/or more buckets.
I would much prefer taking each of the various 'cpu eater' type applications and look at them as a unit. So DSP or convolution or bin packing or list searching and look at what you can do vs what needs help. A lot of the improvements in the x86 architecture came about because a design engineer at Intel or AMD read a clear statement of the problem and the challenges with the solution.
That said, and to justinjlynn's comment, I would really love a top to bottom 'unicode string processing current processors' so many people are doing nothing but scripting these days that string hacking is a big part of interpreters (rather than say raw floating point performance back in the day).
A good approach to getting speedups is probably by assuming ascii, vectorizing the hell out of that, and falling back to multibyte processing where that fails. Checking for multibyte characters comes down to checking if the high bit of any byte is set, which should be fast and easy for the branch predictor to deal with.
Gen -> NEON 6 cycles
NEON -> Gen 8 cycles
So yes, definitely much closer.http://infocenter.arm.com/help/topic/com.arm.doc.uan0015b/Co...
The average word length in English is substantially less than 16, so it is hard to see how this would help.
There are plenty of applications for this algorithm or variations of it. But you are right that the presented application doesn't seem immediately useful.
However, you can just write (x<32), which is defined to be zero or one by the language spec. This shouldn't save anything on ARM, but who knows what the compiler will do.
Similarly, the scalar version might be faster if you do manual loop unrolling. It probably still won't beat Neon, but it makes for a better "scalar vs vectorized" bake-off.
There are just too many ways of doing it. E.g. on most architectures there'll be a straightforward way of doing it using an add/sub operator and taking advantage of overflow/underflow to set a carry flag, and then another operator or three to get the least significant bit set to the carry flag.
Or you can do a straight lookup into a table on any architecture that supports a load from memory and an add, though it'd of course be hugely wasteful.
On any architecture that also supports a right shift / integer division plus AND you could make that use less memory by shifting, masking, lookup up (any set bit =>0, no set bits => 1), and then and'ing the results together with an initial 1.
And many, many more. I have a hard time picturing what an instruction set that would make it impossible to do that without branches would even look like.
Of course that doesn't guarantee it will be the most efficient choice, and of course also doesn't guarantee that a compiler will know how to do it.
And we can often do that: for example a programming language could just ban non-ASCII characters everywhere but in string literals.
But sadly, we do often need to care about all those complicated unicode character classes etc. And if we are talking explicitly about Unicode, then by default we should be considering the whole standard, not just the bits we like.
Also, this space remover looks a lot like a CSV parser, and CSV is a fine high level format if your data is a table (and you care at all about performance).
So I'm not sure "academic" is quite the right word, but I agree that worrying about Unicode is not the point here.
These tricks are in general something that it's well worth learning how to do if you do any performance critical code, as often the boundary of the way that they are implemented in someone's library is not useful for the actual task (i.e. you may have a fast 'find a space' function, but it might not be fast by the time you painfully iterate through each of its results removing spaces one by one). Sadly, a lot of this sort of processing doesn't compose very well unless you have access to an omniscient optimzer.
But yes, if this isn't your hot spot, don't do this thing in particular. It's still possible you might learn something useful by reading about it.
[0] https://groups.google.com/d/msg/comp.arch/QhQDFCcny7w/wVOpIh...
When I really have to get into things, though, I end up writing assembly for the truly hot-loops. In that case, as fugly as x86/64 is, it is one of the best ISAs for exactly expressing the algorithm I have in mind. Personally, I find the most advanced NEON specs are about a decade behind the x86 vector specs. Generally, while the ARM implementations can keep up cycle-for-cycle on commodity parts for scalar, algorithmically, x86/64 blows it out of the water. I think the article cites about 5x? In the worst case, I've seen places where ARM falls entirely out of the I$ and thrashes its D$ & I$ on scalar work-arounds for gaps in its NEON set, where x86/64 manages to just squeak by completely vectorized. In those cases I see 50x (or more) delta, even though the spec sheet would imply that ARM is "just as good" as x86/64.
I like Mike Abrash's quote on this (to paraphrase): "assembly isn't faster than C++; assembly is more expressive than C++".
The typical for 80% of the application
void func(args)
{
asm {
}
}
Literally poor man's macro assembler.Now 40 years of optimizing compiler improvements, we have younger generations thinking that C is like writing Assembly and C compilers were always blazing fast.
So when I see statements about other compilers generating slower code than C ones, I remember those days and smile.