Skip to content

Commit 36c6831

Browse files
kvakilCeres6
authored andcommitted
tools: speedup compilation of js2c output
Incremental compilation of Node.js is slow. Currently on a powerful Linux machine, it takes about 9 seconds and 830 MB of memory to compile `gen/node_javascript.cc` with g++. This is the longest step when recompiling a small change to a Javascript file. `gen/node_javascript.cc` contains a lot of large binary literals of our Javascript source code. It is well-known that embedding large binary literals as C/C++ arrays is slow. One workaround is to include the data as string literals instead. This is particularly nice for the Javascript included via js2c, which look better as string literals anyway. Add a build flag `NODE_JS2C_USE_STRING_LITERALS` to js2c. When this flag is set, we emit string literals instead of array literals, i.e.: ```c++ // old: static const uint8_t X[] = { ... }; static const uint8_t *X = R"JS2C1b732aee(...)JS2C1b732aee"; // old: static const uint16_t Y[] = { ... }; static const uint16_t *Y = uR"JS2C1b732aee(...)JS2C1b732aee"; ``` This requires some modest refactoring in order to deal with the flag being on or off, but the new code itself is actually shorter. I only enabled the new flag on Linux/macOS, since those are systems that I have available for testing. On my Linux system with gcc, it speeds up compilation by 5.5s (9.0s -> 3.5s). On my Mac system with clang, it speeds up compilation by 2.2s (3.7s -> 1.5s). (I don't think this flag will work with MSVC, but it'd probably speed up clang on windows.) The long-term goal here is probably to allow this to occur incrementally per Javascript file & in parallel, to avoid recompiling all of `gen/node_javascript.cc`. Unfortunately the necessary gyp incantations seem impossible (or at least, far beyond me). Anyway, a 60% speedup is a nice enough win. Refs: nodejs#47984 PR-URL: nodejs#48160 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
1 parent 651f37a commit 36c6831

File tree

2 files changed

+95
-31
lines changed

2 files changed

+95
-31
lines changed

node.gyp

+3
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,9 @@
12091209
[ 'node_shared_libuv=="false"', {
12101210
'dependencies': [ 'deps/uv/uv.gyp:libuv#host' ],
12111211
}],
1212+
[ 'OS in "linux mac"', {
1213+
'defines': ['NODE_JS2C_USE_STRING_LITERALS'],
1214+
}],
12121215
[ 'debug_node=="true"', {
12131216
'cflags!': [ '-O3' ],
12141217
'cflags': [ '-g', '-O0' ],

tools/js2c.cc

+92-31
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,19 @@ const std::string& GetCode(uint16_t index) {
390390
return table[index];
391391
}
392392

393+
#ifdef NODE_JS2C_USE_STRING_LITERALS
394+
const char* string_literal_def_template = "static const %s *%s_raw = ";
395+
constexpr std::string_view ascii_string_literal_start =
396+
"reinterpret_cast<const uint8_t*>(R\"JS2C1b732aee(";
397+
constexpr std::string_view utf16_string_literal_start =
398+
"reinterpret_cast<const uint16_t*>(uR\"JS2C1b732aee(";
399+
constexpr std::string_view string_literal_end = ")JS2C1b732aee\");";
400+
#else
401+
const char* array_literal_def_template = "static const %s %s_raw[] = ";
402+
constexpr std::string_view array_literal_start = "{\n";
403+
constexpr std::string_view array_literal_end = "\n};\n\n";
404+
#endif
405+
393406
// Definitions:
394407
// static const uint8_t fs_raw[] = {
395408
// ....
@@ -403,38 +416,93 @@ const std::string& GetCode(uint16_t index) {
403416
//
404417
// static StaticExternalTwoByteResource
405418
// internal_cli_table_resource(internal_cli_table_raw, 1234, nullptr);
406-
constexpr std::string_view literal_end = "\n};\n\n";
419+
//
420+
// If NODE_JS2C_USE_STRING_LITERALS is defined, the data is output as C++
421+
// raw strings (i.e. R"JS2C1b732aee(...)JS2C1b732aee") rather than as an
422+
// array. This speeds up compilation for gcc/clang.
407423
template <typename T>
408-
Fragment GetDefinitionImpl(const std::vector<T>& code, const std::string& var) {
409-
size_t count = code.size();
410-
424+
Fragment GetDefinitionImpl(const std::vector<char>& code,
425+
const std::string& var) {
411426
constexpr bool is_two_byte = std::is_same_v<T, uint16_t>;
412427
static_assert(is_two_byte || std::is_same_v<T, char>);
413-
constexpr size_t unit =
414-
(is_two_byte ? 5 : 3) + 1; // 0-65536 or 0-127 and a ","
428+
429+
size_t count = is_two_byte
430+
? simdutf::utf16_length_from_utf8(code.data(), code.size())
431+
: code.size();
415432
constexpr const char* arr_type = is_two_byte ? "uint16_t" : "uint8_t";
416433
constexpr const char* resource_type = is_two_byte
417434
? "StaticExternalTwoByteResource"
418435
: "StaticExternalOneByteResource";
419436

420-
size_t def_size = 256 + (count * unit);
437+
#ifdef NODE_JS2C_USE_STRING_LITERALS
438+
const char* literal_def_template = string_literal_def_template;
439+
size_t def_size = 512 + code.size();
440+
#else
441+
const char* literal_def_template = array_literal_def_template;
442+
constexpr size_t unit =
443+
(is_two_byte ? 5 : 3) + 1; // 0-65536 or 0-127 and a ","
444+
size_t def_size = 512 + count * unit;
445+
#endif
446+
421447
Fragment result(def_size, 0);
422448

423-
int cur = snprintf(result.data(),
424-
def_size,
425-
"static const %s %s_raw[] = {\n",
426-
arr_type,
427-
var.c_str());
449+
int cur = snprintf(
450+
result.data(), def_size, literal_def_template, arr_type, var.c_str());
451+
428452
assert(cur != 0);
429-
for (size_t i = 0; i < count; ++i) {
453+
454+
#ifdef NODE_JS2C_USE_STRING_LITERALS
455+
constexpr std::string_view start_string_view =
456+
is_two_byte ? utf16_string_literal_start : ascii_string_literal_start;
457+
458+
memcpy(
459+
result.data() + cur, start_string_view.data(), start_string_view.size());
460+
cur += start_string_view.size();
461+
462+
memcpy(result.data() + cur, code.data(), code.size());
463+
cur += code.size();
464+
465+
memcpy(result.data() + cur,
466+
string_literal_end.data(),
467+
string_literal_end.size());
468+
cur += string_literal_end.size();
469+
#else
470+
memcpy(result.data() + cur,
471+
array_literal_start.data(),
472+
array_literal_start.size());
473+
cur += array_literal_start.size();
474+
475+
const std::vector<T>* codepoints;
476+
477+
std::vector<uint16_t> utf16_codepoints;
478+
if constexpr (is_two_byte) {
479+
utf16_codepoints.resize(count);
480+
size_t utf16_count = simdutf::convert_utf8_to_utf16(
481+
code.data(),
482+
code.size(),
483+
reinterpret_cast<char16_t*>(utf16_codepoints.data()));
484+
assert(utf16_count != 0);
485+
utf16_codepoints.resize(utf16_count);
486+
Debug("static size %zu\n", utf16_count);
487+
codepoints = &utf16_codepoints;
488+
} else {
489+
// The code is ASCII, so no need to translate.
490+
codepoints = &code;
491+
}
492+
493+
for (size_t i = 0; i < codepoints->size(); ++i) {
430494
// Avoid using snprintf on large chunks of data because it's much slower.
431495
// It's fine to use it on small amount of data though.
432-
const std::string& str = GetCode(static_cast<uint16_t>(code[i]));
496+
const std::string& str = GetCode(static_cast<uint16_t>((*codepoints)[i]));
497+
433498
memcpy(result.data() + cur, str.c_str(), str.size());
434499
cur += str.size();
435500
}
436-
memcpy(result.data() + cur, literal_end.data(), literal_end.size());
437-
cur += literal_end.size();
501+
502+
memcpy(
503+
result.data() + cur, array_literal_end.data(), array_literal_end.size());
504+
cur += array_literal_end.size();
505+
#endif
438506

439507
int end_size = snprintf(result.data() + cur,
440508
result.size() - cur,
@@ -455,16 +523,9 @@ Fragment GetDefinition(const std::string& var, const std::vector<char>& code) {
455523

456524
if (is_one_byte) {
457525
Debug("static size %zu\n", code.size());
458-
return GetDefinitionImpl(code, var);
526+
return GetDefinitionImpl<char>(code, var);
459527
} else {
460-
size_t length = simdutf::utf16_length_from_utf8(code.data(), code.size());
461-
std::vector<uint16_t> utf16(length);
462-
size_t utf16_count = simdutf::convert_utf8_to_utf16(
463-
code.data(), code.size(), reinterpret_cast<char16_t*>(utf16.data()));
464-
assert(utf16_count != 0);
465-
utf16.resize(utf16_count);
466-
Debug("static size %zu\n", utf16_count);
467-
return GetDefinitionImpl(utf16, var);
528+
return GetDefinitionImpl<uint16_t>(code, var);
468529
}
469530
}
470531

@@ -626,33 +687,33 @@ int JS2C(const FileList& js_files,
626687
const FileList& mjs_files,
627688
const std::string& config,
628689
const std::string& dest) {
629-
Fragments defintions;
630-
defintions.reserve(js_files.size() + mjs_files.size() + 1);
690+
Fragments definitions;
691+
definitions.reserve(js_files.size() + mjs_files.size() + 1);
631692
Fragments initializers;
632693
initializers.reserve(js_files.size() + mjs_files.size());
633694
Fragments registrations;
634695
registrations.reserve(js_files.size() + mjs_files.size() + 1);
635696

636697
for (const auto& filename : js_files) {
637-
int r = AddModule(filename, &defintions, &initializers, &registrations);
698+
int r = AddModule(filename, &definitions, &initializers, &registrations);
638699
if (r != 0) {
639700
return r;
640701
}
641702
}
642703
for (const auto& filename : mjs_files) {
643-
int r = AddModule(filename, &defintions, &initializers, &registrations);
704+
int r = AddModule(filename, &definitions, &initializers, &registrations);
644705
if (r != 0) {
645706
return r;
646707
}
647708
}
648709

649710
assert(config == "config.gypi");
650711
// "config.gypi" -> config_raw.
651-
int r = AddGypi("config", config, &defintions);
712+
int r = AddGypi("config", config, &definitions);
652713
if (r != 0) {
653714
return r;
654715
}
655-
Fragment out = Format(defintions, initializers, registrations);
716+
Fragment out = Format(definitions, initializers, registrations);
656717
return WriteIfChanged(out, dest);
657718
}
658719

0 commit comments

Comments
 (0)