Repository: nodejs/node
Pull request: src: improve StringBytes::Encode perf on ASCII

Tracking: #61041 

This significantly improves both utf8 `TextDecoder` and `Buffer#toString()` performance on ASCII

And removes the main reason why `import { TextDecoder } from '@exodus/bytes/encoding.js'` beats both Node.js TextDecoder and Node.js Buffer on Node.js on utf-8 🙃 

See https://github.com/nodejs/node/issues/61041#issuecomment-3658249474

Using https://github.com/lemire/jstextdecoderbench by @lemire:

### Buffer#toString() - utf8

main:
| Test | Size | Throughput | Mean Time |
|------|------|------------|-----------|
| Latin lipsum (ASCII) | 84.902 KiB | 17.60 GiB/s | 0.005 ms |
| Arabic lipsum | 79.771 KiB | 0.26 GiB/s | 0.294 ms |
| Chinese lipsum | 68.203 KiB | 0.32 GiB/s | 0.212 ms |

PR:
| Test | Size | Throughput | Mean Time |
|------|------|------------|-----------|
| Latin lipsum (ASCII) | 84.902 KiB | 36.74 GiB/s | 0.002 ms |
| Arabic lipsum | 79.771 KiB | 0.27 GiB/s | 0.280 ms |
| Chinese lipsum | 68.203 KiB | 0.33 GiB/s | 0.199 ms |

### TextDecoder, loose

main:
| Test | Size | Throughput | Mean Time |
|------|------|------------|-----------|
| Latin lipsum (ASCII) | 84.902 KiB | 17.90 GiB/s | 0.005 ms |
| Arabic lipsum | 79.771 KiB | 0.27 GiB/s | 0.286 ms |
| Chinese lipsum | 68.203 KiB | 0.33 GiB/s | 0.200 ms |

PR:
| Test | Size | Throughput | Mean Time |
|------|------|------------|-----------|
| Latin lipsum (ASCII) | 84.902 KiB | 36.61 GiB/s | 0.002 ms |
| Arabic lipsum | 79.771 KiB | 0.27 GiB/s | 0.279 ms |
| Chinese lipsum | 68.203 KiB | 0.33 GiB/s | 0.208 ms |

### TextDecoder, fatal

main:
| Test | Size | Throughput | Mean Time |
|------|------|------------|-----------|
| Latin lipsum (ASCII) | 84.902 KiB | 15.17 GiB/s | 0.006 ms |
| Arabic lipsum | 79.771 KiB | 0.26 GiB/s | 0.292 ms |
| Chinese lipsum | 68.203 KiB | 0.32 GiB/s | 0.206 ms |

PR:
| Test | Size | Throughput | Mean Time |
|------|------|------------|-----------|
| Latin lipsum (ASCII) | 84.902 KiB | 36.57 GiB/s | 0.002 ms |
| Arabic lipsum | 79.771 KiB | 0.27 GiB/s | 0.286 ms |
| Chinese lipsum | 68.203 KiB | 0.31 GiB/s | 0.207 ms |


---

cc @nodejs/performance 

Changed files:
filename: src/encoding_binding.cc
patch: @@ -356,7 +356,24 @@ void BindingData::DecodeUTF8(const FunctionCallbackInfo<Value>& args) {
   const char* data = buffer.data();
   size_t length = buffer.length();
 
+  if (!ignore_bom && length >= 3) {
+    if (memcmp(data, "\xEF\xBB\xBF", 3) == 0) {
+      data += 3;
+      length -= 3;
+    }
+  }
+
   if (has_fatal) {
+    // Are we perhaps ASCII? Then we won't have to check for UTF-8
+    if (!simdutf::validate_ascii_with_errors(data, length).error) {
+      Local<Value> ret;
+      if (StringBytes::Encode(env->isolate(), data, length, LATIN1)
+              .ToLocal(&ret)) {
+        args.GetReturnValue().Set(ret);
+      }
+      return;
+    }
+
     auto result = simdutf::validate_utf8_with_errors(data, length);
 
     if (result.error) {
@@ -365,13 +382,6 @@ void BindingData::DecodeUTF8(const FunctionCallbackInfo<Value>& args) {
     }
   }
 
-  if (!ignore_bom && length >= 3) {
-    if (memcmp(data, "\xEF\xBB\xBF", 3) == 0) {
-      data += 3;
-      length -= 3;
-    }
-  }
-
   if (length == 0) return args.GetReturnValue().SetEmptyString();
 
   Local<Value> ret;

filename: src/string_bytes.cc
patch: @@ -531,6 +531,14 @@ MaybeLocal<Value> StringBytes::Encode(Isolate* isolate,
 
     case UTF8: {
       buflen = keep_buflen_in_range(buflen);
+
+      // ASCII fast path
+      // TODO(chalker): remove when String::NewFromUtf8 is fast enough itself
+      // This is cheap compared to the benefits though
+      if (!simdutf::validate_ascii_with_errors(buf, buflen).error) {
+        return ExternOneByteString::NewFromCopy(isolate, buf, buflen);
+      }
+
       val =
           String::NewFromUtf8(isolate, buf, v8::NewStringType::kNormal, buflen);
       Local<String> str;