TE
TechEcho
Home24h TopNewestBestAskShowJobs
GitHubTwitter
Home

TechEcho

A tech news platform built with Next.js, providing global tech news and discussions.

GitHubTwitter

Home

HomeNewestBestAskShowJobs

Resources

HackerNews APIOriginal HackerNewsNext.js

© 2025 TechEcho. All rights reserved.

How quickly can you remove spaces from a string?

201 pointsby deafcalculusover 8 years ago

25 comments

jakobeggerover 8 years ago
Fun story: When I implemented the syntax highlighting feature in Sequel Pro, I needed a way to determine the string length of UTF-8 encoded strings. I didn&#x27;t find a function available on macOS that worked directly on a char*, so I googled and found a really simple UTF-8 strlen function. It was easy to understand and very fast. I think it was this: <a href="http:&#x2F;&#x2F;canonical.org&#x2F;~kragen&#x2F;strlen-utf8.html" rel="nofollow">http:&#x2F;&#x2F;canonical.org&#x2F;~kragen&#x2F;strlen-utf8.html</a><p>I committed my syntax highlighting code, and a few days later someone had replaced the simple UTF8 strlen function with the really long vectorised version from this page: <a href="http:&#x2F;&#x2F;www.daemonology.net&#x2F;blog&#x2F;2008-06-05-faster-utf8-strlen.html" rel="nofollow">http:&#x2F;&#x2F;www.daemonology.net&#x2F;blog&#x2F;2008-06-05-faster-utf8-strle...</a><p>But the funny thing is, that the supposedly fast vectorised strlen was optimised for very long strings. The benchmarks shows results for megabytes of texts. But we measured the length of tokens, usually only a few characters long, so in most cases the new function was actually slower!<p>I was a very junior dev, didn&#x27;t want to piss off anybody, and the strlen wasn&#x27;t in the hot path anyway, so I didn&#x27;t say anything. But I was a bit sad, that my easy-to-read code was replaced by such a monstrosity.<p>What&#x27;s my point? Before you go and use these functions in your code, profile your code to see if it would actually affect performance.
dzdtover 8 years ago
Somehow the article fails to mention that the speed will depend heavily on the input string: both its size and distribution of whitespace characters.<p>Even assuming the question is for the limit of very long strings, the distribution makes a huge difference. Natural English has spaces on average every 5.1 characters [1], so using multi-character tests to speed up the case of runs of 8 or more characters without a space will probably slow it down, not speed it up!<p>[1]<a href="https:&#x2F;&#x2F;arxiv.org&#x2F;pdf&#x2F;1208.6109" rel="nofollow">https:&#x2F;&#x2F;arxiv.org&#x2F;pdf&#x2F;1208.6109</a>
评论 #13447269 未加载
评论 #13447625 未加载
评论 #13447295 未加载
评论 #13447942 未加载
computatorover 8 years ago
What strikes me is how many sites won&#x27;t accept a login name, credit card number, phone number, or other field with leading or trailing spaces. This for something where the speed of the code doesn&#x27;t matter at all. I can&#x27;t think of any explanation other than incredible laziness or incompetence for not stripping off spaces.<p>You might not notice this if you use a password manager or browser autofill, but it&#x27;s a lot of sites, including companies like major airlines for example.<p>Never mind that -- it&#x27;s just a miracle when you can enter a credit card number formatted as 4123 4567 8901 2345 rather than squished together.
评论 #13451114 未加载
评论 #13449411 未加载
dsp1234over 8 years ago
<i>This code will work on all UTF-8 encoded strings… which is the bulk of the strings found on the Internet if you consider that UTF-8 is a superset of ASCII.</i><p>Note that the first set of code (and possibly the rest), only work as the space, newline, and carriage return are the 7 bit ASCII set is included in UTF-8. However, the extended 8-bit ASCII set is not, but is often included when people speak of ASCII. So for example, if the request was to remove all &quot;Copyright Sign&quot; symbols, which is U00A9, it would not work correctly. The UTF-8 encoding for this symbol is 0xC2 0xA9, but the code only works on individual bytes, so it would remove the A9 byte, leaving a C2 byte and then whatever byte came next. Additionally, it would hit other UTF-8 characters like the &quot;Greek Capital Letter Omega&quot; (Ω which is encoded in UTF-8 as 0xCE 0xA9)<p>tl;dr Only works for the 7-bit ASCII set, but not the common extended 8-bit ASCII sets
评论 #13446764 未加载
评论 #13446905 未加载
评论 #13446683 未加载
评论 #13447556 未加载
nograciasover 8 years ago
6.5MB for the tables to drive this faster de-spacer? No thank you. It would blow out the L1 cache and make your program slower.<p><a href="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;lemire&#x2F;despacer&#x2F;master&#x2F;include&#x2F;despacer_tables.h" rel="nofollow">https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;lemire&#x2F;despacer&#x2F;master&#x2F;inc...</a>
评论 #13448432 未加载
et1337over 8 years ago
Cached text-only version: <a href="http:&#x2F;&#x2F;webcache.googleusercontent.com&#x2F;search?q=cache%3Ahttp%3A%2F%2Flemire.me%2Fblog%2F2017%2F01%2F20%2Fhow-quickly-can-you-remove-spaces-from-a-string%2F&amp;strip=1" rel="nofollow">http:&#x2F;&#x2F;webcache.googleusercontent.com&#x2F;search?q=cache%3Ahttp%...</a>
brianpgordonover 8 years ago
This reminded me of spray-json&#x27;s JsonParser. It has a bit of Scala code to seek past whitespace:<p><pre><code> @tailrec private def ws(): Unit = &#x2F;&#x2F; fast test whether cursorChar is one of &quot; \n\r\t&quot; if (((1L &lt;&lt; cursorChar) &amp; ((cursorChar - 64) &gt;&gt; 31) &amp; 0x100002600L) != 0L) { advance(); ws() } </code></pre> <a href="https:&#x2F;&#x2F;github.com&#x2F;spray&#x2F;spray-json&#x2F;blob&#x2F;765c83248e0bbe867ddc9d479b4fe79493569a54&#x2F;src&#x2F;main&#x2F;scala&#x2F;spray&#x2F;json&#x2F;JsonParser.scala#L186" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;spray&#x2F;spray-json&#x2F;blob&#x2F;765c83248e0bbe867dd...</a>
评论 #13448946 未加载
octo_tover 8 years ago
So using 128bits instructions would imply you had &#x27;words&#x27; which were over 16 characters long on average, right?<p>The average (english) word is ~5 characters long, so most of the time, you&#x27;d be forced to check anyway.
评论 #13446578 未加载
评论 #13446566 未加载
mnarayan01over 8 years ago
I feel like you have to at <i>least</i> include non-breaking spaces if you&#x27;re going to say you&#x27;re removing spaces from UTF-8 strings.
criddellover 8 years ago
Is there a clever way to remove all the space characters?<p><a href="https:&#x2F;&#x2F;www.cs.tut.fi&#x2F;~jkorpela&#x2F;chars&#x2F;spaces.html" rel="nofollow">https:&#x2F;&#x2F;www.cs.tut.fi&#x2F;~jkorpela&#x2F;chars&#x2F;spaces.html</a>
评论 #13448595 未加载
评论 #13446697 未加载
stelundover 8 years ago
Maybe the asm to do scan for byte in memory is an alternative. Repne scab.<p>It won&#x27;t find all characters in a single scan. But maybe do 3 passes over a buffer which fits in cache.
aibover 8 years ago
I wonder how much of a difference using a separate destination buffer would make. Apart from some memory&#x2F;cache&#x2F;invalidation&#x2F;magic thing that I&#x27;m not certain might occur, it would allow one to use the extended instructions used for scanning.
Annatarover 8 years ago
<p><pre><code> awk &#x27; { gsub(&#x2F;[[:blank:]\015]&#x2F;, &quot;&quot;); printf(&quot;%s&quot;, $0); }&#x27; input | tee output </code></pre> stripping out LF isn&#x27;t necessary because AWK does it on every record automatically. For even more speed, the code can be translated into ANSI C and compiled with awka[1] using an optimizing C compiler.<p>[1] <a href="http:&#x2F;&#x2F;awka.sourceforge.net&#x2F;index.html" rel="nofollow">http:&#x2F;&#x2F;awka.sourceforge.net&#x2F;index.html</a>
exDM69over 8 years ago
I am surprised to see any speedup in this! I&#x27;d expect something trivial like this to be completely memory bound with the CPU sitting almost idle waiting for bytes coming in from memory.<p>Looking at the benchmark code, this is using rdtsc to read the CPU time stamp counter. That does not take waiting for memory into account, does it?<p>I wonder if there&#x27;s a difference when measured in wall clock time. It&#x27;s still somewhat beneficial to have the CPU work efficiently to give an opportunity for hyperthreading to take place when waiting for memory.<p>If you really wanted to make something like this faster, you should focus on cache utilization and make use of prefetching instructions. x86 has pretty bad prefetching instructions and pretty good speculative fetching, so don&#x27;t expect massive speedups but on ARM or Aarch64, you have a finer grained control over cache prefetching (L1 and L2 separately) and you could see <i>much</i> bigger differences.<p>As for benchmarking this kind of problems: you obviously want to measure real world performance, so you need wall clock time as well as time stamp counter, but I&#x27;d look for optimization clues in &quot;perf stat&quot; and other CPU perf counters, with an emphasis on cache misses and branch mispredictions.<p>The figure you should be staring at is the total <i>throughput</i> of the algorithm, measured in gigabytes per second. You should be getting figures close to the memory bandwidth available (25-50 GB&#x2F;s depending on CPU and memory).<p>edit: I measured the wall clock time with clock_gettime before&#x2F;after all the repeats (using a megabyte sized buffer) and there is indeed no significant difference, here&#x27;s my results:<p><pre><code> memcpy(tmpbuffer,buffer,N): 0.122945 cycles &#x2F; ops 1495907352 nsec (1.495907 sec) countspaces(buffer, N): 3.657322 cycles &#x2F; ops 1544915395 nsec (1.544915 sec) despace(buffer, N): 6.521193 cycles &#x2F; ops 1621204460 nsec (1.621204 sec) faster_despace(buffer, N): 1.721657 cycles &#x2F; ops 1500507217 nsec (1.500507 sec) despace64(buffer, N): 3.595031 cycles &#x2F; ops 1544993649 nsec (1.544994 sec) despace_to(buffer, N, tmpbuffer): 6.307885 cycles &#x2F; ops 1615101563 nsec (1.615102 sec) avx2_countspaces(buffer, N): 0.190992 cycles &#x2F; ops 1460961459 nsec (1.460961 sec) avx2_despace(buffer, N): 5.750583 cycles &#x2F; ops 1615971010 nsec (1.615971 sec) sse4_despace(buffer, N): 0.985002 cycles &#x2F; ops 1482901389 nsec (1.482901 sec) sse4_despace_branchless(buffer, N): 0.338737 cycles &#x2F; ops 1460874704 nsec (1.460875 sec) sse4_despace_trail(buffer, N): 1.950657 cycles &#x2F; ops 1502268447 nsec (1.502268 sec) sse42_despace_branchless(buffer, N): 0.562246 cycles &#x2F; ops 1468638389 nsec (1.468638 sec) sse42_despace_branchless_lookup(buffer, N): 0.624913 cycles &#x2F; ops 1472445127 nsec (1.472445 sec) sse42_despace_to(buffer, N,tmpbuffer): 1.747046 cycles &#x2F; ops 1507705780 nsec (1.507706 sec) </code></pre> Here&#x27;s the diff to the original: <a href="http:&#x2F;&#x2F;pasteall.org&#x2F;208511" rel="nofollow">http:&#x2F;&#x2F;pasteall.org&#x2F;208511</a><p>edit2: surprisingly, Clang is about 10% slower than GCC in my experiments.
评论 #13450860 未加载
chrismorganover 8 years ago
A variant of this problem yields further interesting possibilities: if you’re trying to remove control characters like CR and LF, but replacing them with whitespace would be acceptable. That way you can work on it in-place, without needing to copy memory or allocate or anything like that.
saretiredover 8 years ago
The “optimized” functions have a bug when the number of remaining bytes is less than the block size.
评论 #13447284 未加载
Tooover 8 years ago
Someone file a compiler bug? 14x difference between the readable code and the optimized code is a lot. The first code is extremely straightforward, you shouldn&#x27;t have deal with that SIMD mess manually.
评论 #13449607 未加载
评论 #13450200 未加载
fisherjeffover 8 years ago
Adding lookup tables to the naïve implementation gave me a ~3x speedup with virtually no extra effort. Branch penalties are a real killer.
评论 #13448410 未加载
ascotanover 8 years ago
I guess you could write CUDA code to do this on a GPU, but then the question becomes why? :&#x2F;
评论 #13449110 未加载
评论 #13448996 未加载
评论 #13449271 未加载
Dargeover 8 years ago
Does anyone know how to do such a benchmark?
评论 #13447453 未加载
ebbvover 8 years ago
Optimizing by hand is rarely the fastest thing to do nowadays. I wonder how the original naive approach fairs with all optimizations turned on for gcc and with realistic input?
评论 #13449429 未加载
cowardlydragonover 8 years ago
ASCII&#x2F;1 byte characters? I stopped reading then.<p>UNICODE or GTFO.<p>Should be embarassingly parallel for a contiguous array.
评论 #13448883 未加载
johnnyb9over 8 years ago
Going to start using this as an interview question!
评论 #13448527 未加载
austincheneyover 8 years ago
Isn&#x27;t this the very kind of thing Regular Expressions were created to solve?<p>* Remove spaces - myString.replace(&#x2F;\u0020+&#x2F;g, &quot;&quot;);<p>* Remove common line terminators - myString.replace(&#x2F;(\r|\n)+&#x2F;g, &quot;&quot;);<p>* Remove all white space - myString.replace(&#x2F;\s+&#x2F;g, &quot;&quot;);
评论 #13446714 未加载
评论 #13446744 未加载
hcrispover 8 years ago
Not claiming to be the fastest, but here are two Python solutions suggested by Dave Beazley [0], plus one I extended using compiled regular expressions:<p><pre><code> # string replacement s = &#x27; hello world \n&#x27; s.replace(&#x27; &#x27;, &#x27;&#x27;) # &#x27;helloworld&#x27; # regular expression import re re.sub(&#x27;\s+&#x27;, &#x27;&#x27;, s) # &#x27;helloworld&#x27; # compiled regular expression pat = re.compile(&#x27;\s+&#x27;) pat.sub(&#x27;&#x27;, s) # &#x27;helloworld&#x27; </code></pre> [0] <a href="https:&#x2F;&#x2F;www.google.com&#x2F;url?sa=t&amp;rct=j&amp;q=&amp;esrc=s&amp;source=web&amp;cd=1&amp;cad=rja&amp;uact=8&amp;ved=0ahUKEwiDwpPQytHRAhVJWCwKHSTmD5wQFggaMAA&amp;url=https%3A%2F%2Fwww.safaribooksonline.com%2Flibrary%2Fview%2Fpython-cookbook-3rd%2F9781449357337%2Fch02s11.html&amp;usg=AFQjCNEZqL6SV76UmE4ojyJ5poOgeCexEQ&amp;sig2=I9kGVDVg5DyLxZ3pCJ1FIw&amp;bvm=bv.144224172,d.bGg" rel="nofollow">https:&#x2F;&#x2F;www.google.com&#x2F;url?sa=t&amp;rct=j&amp;q=&amp;esrc=s&amp;source=web&amp;c...</a>
评论 #13446851 未加载