Skip to content

Commit 9fd8109

Browse files
committed
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 new flag `--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: #47984
1 parent 817c579 commit 9fd8109

File tree

2 files changed

+119
-32
lines changed

2 files changed

+119
-32
lines changed

node.gyp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,10 +938,18 @@
938938
'action': [
939939
'<(node_js2c_exec)',
940940
'<@(_outputs)',
941+
'<@(node_js2c_use_string_literals_flag)',
941942
'lib',
942943
'config.gypi',
943944
'<@(deps_files)',
944945
],
946+
'conditions': [
947+
['OS=="linux" or OS=="mac"', {
948+
'variables': {'node_js2c_use_string_literals_flag': ['--use-string-literals']},
949+
}, {
950+
'variables': {'node_js2c_use_string_literals_flag': []},
951+
}]
952+
],
945953
},
946954
],
947955
}, # node_lib_target_name

tools/js2c.cc

Lines changed: 111 additions & 32 deletions
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+
// Used when use_string_literals = true.
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+
401+
// Used when use_string_literals = false.
402+
const char* array_literal_def_template = "static const %s %s_raw[] = ";
403+
constexpr std::string_view array_literal_start = "{\n";
404+
constexpr std::string_view array_literal_end = "\n};\n\n";
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 use_string_literals is set, the data is output as C++ raw strings
421+
// (i.e. R"JS2C1b732aee(...)JS2C1b732aee") rather than as an array. This speeds
422+
// 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,
426+
bool use_string_literals) {
411427
constexpr bool is_two_byte = std::is_same_v<T, uint16_t>;
412428
static_assert(is_two_byte || std::is_same_v<T, char>);
429+
430+
size_t count = is_two_byte
431+
? simdutf::utf16_length_from_utf8(code.data(), code.size())
432+
: code.size();
413433
constexpr size_t unit =
414434
(is_two_byte ? 5 : 3) + 1; // 0-65536 or 0-127 and a ","
415435
constexpr const char* arr_type = is_two_byte ? "uint16_t" : "uint8_t";
416436
constexpr const char* resource_type = is_two_byte
417437
? "StaticExternalTwoByteResource"
418438
: "StaticExternalOneByteResource";
419439

420-
size_t def_size = 256 + (count * unit);
440+
size_t def_size = 512 + (use_string_literals ? code.size() : count * unit);
421441
Fragment result(def_size, 0);
422442

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

439507
int end_size = snprintf(result.data() + cur,
440508
result.size() - cur,
@@ -448,30 +516,26 @@ Fragment GetDefinitionImpl(const std::vector<T>& code, const std::string& var) {
448516
return result;
449517
}
450518

451-
Fragment GetDefinition(const std::string& var, const std::vector<char>& code) {
519+
Fragment GetDefinition(const std::string& var,
520+
const std::vector<char>& code,
521+
bool use_string_literals) {
452522
Debug("GetDefinition %s, code size %zu ", var.c_str(), code.size());
453523
bool is_one_byte = simdutf::validate_ascii(code.data(), code.size());
454524
Debug("with %s\n", is_one_byte ? "1-byte chars" : "2-byte chars");
455525

456526
if (is_one_byte) {
457527
Debug("static size %zu\n", code.size());
458-
return GetDefinitionImpl(code, var);
528+
return GetDefinitionImpl<char>(code, var, use_string_literals);
459529
} 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);
530+
return GetDefinitionImpl<uint16_t>(code, var, use_string_literals);
468531
}
469532
}
470533

471534
int AddModule(const std::string& filename,
472535
Fragments* definitions,
473536
Fragments* initializers,
474-
Fragments* registrations) {
537+
Fragments* registrations,
538+
bool use_string_literals) {
475539
Debug("AddModule %s start\n", filename.c_str());
476540

477541
int error = 0;
@@ -486,7 +550,7 @@ int AddModule(const std::string& filename,
486550
std::string file_id = GetFileId(filename);
487551
std::string var = GetVariableName(file_id);
488552

489-
definitions->emplace_back(GetDefinition(var, code));
553+
definitions->emplace_back(GetDefinition(var, code, use_string_literals));
490554

491555
// Initializers of the BuiltinSourceMap:
492556
// {"fs", UnionBytes{&fs_resource}},
@@ -603,6 +667,7 @@ std::vector<char> JSONify(const std::vector<char>& code) {
603667

604668
int AddGypi(const std::string& var,
605669
const std::string& filename,
670+
bool use_string_literals,
606671
Fragments* definitions) {
607672
Debug("AddGypi %s start\n", filename.c_str());
608673

@@ -618,14 +683,16 @@ int AddGypi(const std::string& var,
618683
assert(var == "config");
619684

620685
std::vector<char> transformed = JSONify(code);
621-
definitions->emplace_back(GetDefinition(var, transformed));
686+
definitions->emplace_back(
687+
GetDefinition(var, transformed, use_string_literals));
622688
return 0;
623689
}
624690

625691
int JS2C(const FileList& js_files,
626692
const FileList& mjs_files,
627693
const std::string& config,
628-
const std::string& dest) {
694+
const std::string& dest,
695+
bool use_string_literals) {
629696
Fragments defintions;
630697
defintions.reserve(js_files.size() + mjs_files.size() + 1);
631698
Fragments initializers;
@@ -634,21 +701,29 @@ int JS2C(const FileList& js_files,
634701
registrations.reserve(js_files.size() + mjs_files.size() + 1);
635702

636703
for (const auto& filename : js_files) {
637-
int r = AddModule(filename, &defintions, &initializers, &registrations);
704+
int r = AddModule(filename,
705+
&defintions,
706+
&initializers,
707+
&registrations,
708+
use_string_literals);
638709
if (r != 0) {
639710
return r;
640711
}
641712
}
642713
for (const auto& filename : mjs_files) {
643-
int r = AddModule(filename, &defintions, &initializers, &registrations);
714+
int r = AddModule(filename,
715+
&defintions,
716+
&initializers,
717+
&registrations,
718+
use_string_literals);
644719
if (r != 0) {
645720
return r;
646721
}
647722
}
648723

649724
assert(config == "config.gypi");
650725
// "config.gypi" -> config_raw.
651-
int r = AddGypi("config", config, &defintions);
726+
int r = AddGypi("config", config, use_string_literals, &defintions);
652727
if (r != 0) {
653728
return r;
654729
}
@@ -673,6 +748,7 @@ int Main(int argc, char* argv[]) {
673748
std::vector<std::string> args;
674749
args.reserve(argc);
675750
std::string root_dir;
751+
bool use_string_literals = false;
676752
for (int i = 1; i < argc; ++i) {
677753
std::string arg(argv[i]);
678754
if (arg == "--verbose") {
@@ -683,6 +759,8 @@ int Main(int argc, char* argv[]) {
683759
return 1;
684760
}
685761
root_dir = argv[++i];
762+
} else if (arg == "--use-string-literals") {
763+
use_string_literals = true;
686764
} else {
687765
args.emplace_back(argv[i]);
688766
}
@@ -744,7 +822,8 @@ int Main(int argc, char* argv[]) {
744822
std::sort(js_it->second.begin(), js_it->second.end());
745823
std::sort(mjs_it->second.begin(), mjs_it->second.end());
746824

747-
return JS2C(js_it->second, mjs_it->second, config, output);
825+
return JS2C(
826+
js_it->second, mjs_it->second, config, output, use_string_literals);
748827
}
749828
} // namespace js2c
750829
} // namespace node

0 commit comments

Comments
 (0)