Skip to content

Commit

Permalink
tools: speedup compilation of js2c output
Browse files Browse the repository at this point in the history
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: nodejs#47984
  • Loading branch information
kvakil committed May 25, 2023
1 parent 817c579 commit 80ed2c0
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 33 deletions.
8 changes: 8 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -938,10 +938,18 @@
'action': [
'<(node_js2c_exec)',
'<@(_outputs)',
'<@(node_js2c_use_string_literals_flag)',
'lib',
'config.gypi',
'<@(deps_files)',
],
'conditions': [
['OS=="linux" or OS=="mac"', {
'variables': {'node_js2c_use_string_literals_flag': ['--use-string-literals']},
}, {
'variables': {'node_js2c_use_string_literals_flag': []},
}]
],
},
],
}, # node_lib_target_name
Expand Down
142 changes: 109 additions & 33 deletions tools/js2c.cc
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,17 @@ const std::string& GetCode(uint16_t index) {
return table[index];
}

const char* string_literal_def_template = "static const %s *%s_raw = ";
constexpr std::string_view ascii_string_literal_start =
"reinterpret_cast<const uint8_t*>(R\"JS2C1b732aee(";
constexpr std::string_view utf16_string_literal_start =
"reinterpret_cast<const uint16_t*>(uR\"JS2C1b732aee(";
constexpr std::string_view string_literal_end = ")JS2C1b732aee\");";

const char* array_literal_def_template = "static const %s %s_raw[] = ";
constexpr std::string_view array_literal_start = "{\n";
constexpr std::string_view array_literal_end = "\n};\n\n";

// Definitions:
// static const uint8_t fs_raw[] = {
// ....
Expand All @@ -403,38 +414,92 @@ const std::string& GetCode(uint16_t index) {
//
// static StaticExternalTwoByteResource
// internal_cli_table_resource(internal_cli_table_raw, 1234, nullptr);
constexpr std::string_view literal_end = "\n};\n\n";
//
// If use_string_literals is set, the data is output as C++ raw strings
// (i.e. R"JS2C1b732aee(...)JS2C1b732aee") rather than as an array. This speeds
// up compilation for gcc/clang.
template <typename T>
Fragment GetDefinitionImpl(const std::vector<T>& code, const std::string& var) {
size_t count = code.size();

Fragment GetDefinitionImpl(const std::vector<char>& code,
const std::string& var,
bool use_string_literals) {
constexpr bool is_two_byte = std::is_same_v<T, uint16_t>;
static_assert(is_two_byte || std::is_same_v<T, char>);

size_t count = is_two_byte
? simdutf::utf16_length_from_utf8(code.data(), code.size())
: code.size();
constexpr size_t unit =
(is_two_byte ? 5 : 3) + 1; // 0-65536 or 0-127 and a ","
constexpr const char* arr_type = is_two_byte ? "uint16_t" : "uint8_t";
constexpr const char* resource_type = is_two_byte
? "StaticExternalTwoByteResource"
: "StaticExternalOneByteResource";

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

int cur = snprintf(result.data(),
def_size,
"static const %s %s_raw[] = {\n",
use_string_literals ? string_literal_def_template
: array_literal_def_template,
arr_type,
var.c_str());
assert(cur != 0);
for (size_t i = 0; i < count; ++i) {
// Avoid using snprintf on large chunks of data because it's much slower.
// It's fine to use it on small amount of data though.
const std::string& str = GetCode(static_cast<uint16_t>(code[i]));
memcpy(result.data() + cur, str.c_str(), str.size());
cur += str.size();

if (use_string_literals) {
constexpr std::string_view start_string_view =
is_two_byte ? utf16_string_literal_start : ascii_string_literal_start;

memcpy(result.data() + cur,
start_string_view.data(),
start_string_view.size());
cur += start_string_view.size();

memcpy(result.data() + cur, code.data(), code.size());
cur += code.size();

memcpy(result.data() + cur,
string_literal_end.data(),
string_literal_end.size());
cur += string_literal_end.size();
} else {
assert(cur != 0);

memcpy(result.data() + cur,
array_literal_start.data(),
array_literal_start.size());
cur += array_literal_start.size();

std::vector<uint16_t> utf16_codepoints;
const std::vector<T>* codepoints;
if constexpr (is_two_byte) {
size_t length = simdutf::utf16_length_from_utf8(code.data(), code.size());
utf16_codepoints.resize(length);
size_t utf16_count = simdutf::convert_utf8_to_utf16(
code.data(),
code.size(),
reinterpret_cast<char16_t*>(utf16_codepoints.data()));
assert(utf16_count != 0);
utf16_codepoints.resize(utf16_count);
Debug("static size %zu\n", utf16_count);
codepoints = &utf16_codepoints;
} else {
codepoints = &code;
}

for (size_t i = 0; i < codepoints->size(); ++i) {
// Avoid using snprintf on large chunks of data because it's much slower.
// It's fine to use it on small amount of data though.
const std::string& str = GetCode(static_cast<uint16_t>((*codepoints)[i]));

memcpy(result.data() + cur, str.c_str(), str.size());
cur += str.size();
}

memcpy(result.data() + cur,
array_literal_end.data(),
array_literal_end.size());
cur += array_literal_end.size();
}
memcpy(result.data() + cur, literal_end.data(), literal_end.size());
cur += literal_end.size();

int end_size = snprintf(result.data() + cur,
result.size() - cur,
Expand All @@ -448,30 +513,26 @@ Fragment GetDefinitionImpl(const std::vector<T>& code, const std::string& var) {
return result;
}

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

if (is_one_byte) {
Debug("static size %zu\n", code.size());
return GetDefinitionImpl(code, var);
return GetDefinitionImpl<char>(code, var, use_string_literals);
} else {
size_t length = simdutf::utf16_length_from_utf8(code.data(), code.size());
std::vector<uint16_t> utf16(length);
size_t utf16_count = simdutf::convert_utf8_to_utf16(
code.data(), code.size(), reinterpret_cast<char16_t*>(utf16.data()));
assert(utf16_count != 0);
utf16.resize(utf16_count);
Debug("static size %zu\n", utf16_count);
return GetDefinitionImpl(utf16, var);
return GetDefinitionImpl<uint16_t>(code, var, use_string_literals);
}
}

int AddModule(const std::string& filename,
Fragments* definitions,
Fragments* initializers,
Fragments* registrations) {
Fragments* registrations,
bool use_string_literals) {
Debug("AddModule %s start\n", filename.c_str());

int error = 0;
Expand All @@ -486,7 +547,7 @@ int AddModule(const std::string& filename,
std::string file_id = GetFileId(filename);
std::string var = GetVariableName(file_id);

definitions->emplace_back(GetDefinition(var, code));
definitions->emplace_back(GetDefinition(var, code, use_string_literals));

// Initializers of the BuiltinSourceMap:
// {"fs", UnionBytes{&fs_resource}},
Expand Down Expand Up @@ -603,6 +664,7 @@ std::vector<char> JSONify(const std::vector<char>& code) {

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

Expand All @@ -618,14 +680,16 @@ int AddGypi(const std::string& var,
assert(var == "config");

std::vector<char> transformed = JSONify(code);
definitions->emplace_back(GetDefinition(var, transformed));
definitions->emplace_back(
GetDefinition(var, transformed, use_string_literals));
return 0;
}

int JS2C(const FileList& js_files,
const FileList& mjs_files,
const std::string& config,
const std::string& dest) {
const std::string& dest,
bool use_string_literals) {
Fragments defintions;
defintions.reserve(js_files.size() + mjs_files.size() + 1);
Fragments initializers;
Expand All @@ -634,21 +698,29 @@ int JS2C(const FileList& js_files,
registrations.reserve(js_files.size() + mjs_files.size() + 1);

for (const auto& filename : js_files) {
int r = AddModule(filename, &defintions, &initializers, &registrations);
int r = AddModule(filename,
&defintions,
&initializers,
&registrations,
use_string_literals);
if (r != 0) {
return r;
}
}
for (const auto& filename : mjs_files) {
int r = AddModule(filename, &defintions, &initializers, &registrations);
int r = AddModule(filename,
&defintions,
&initializers,
&registrations,
use_string_literals);
if (r != 0) {
return r;
}
}

assert(config == "config.gypi");
// "config.gypi" -> config_raw.
int r = AddGypi("config", config, &defintions);
int r = AddGypi("config", config, use_string_literals, &defintions);
if (r != 0) {
return r;
}
Expand All @@ -673,6 +745,7 @@ int Main(int argc, char* argv[]) {
std::vector<std::string> args;
args.reserve(argc);
std::string root_dir;
bool use_string_literals = false;
for (int i = 1; i < argc; ++i) {
std::string arg(argv[i]);
if (arg == "--verbose") {
Expand All @@ -683,6 +756,8 @@ int Main(int argc, char* argv[]) {
return 1;
}
root_dir = argv[++i];
} else if (arg == "--use-string-literals") {
use_string_literals = true;
} else {
args.emplace_back(argv[i]);
}
Expand Down Expand Up @@ -744,7 +819,8 @@ int Main(int argc, char* argv[]) {
std::sort(js_it->second.begin(), js_it->second.end());
std::sort(mjs_it->second.begin(), mjs_it->second.end());

return JS2C(js_it->second, mjs_it->second, config, output);
return JS2C(
js_it->second, mjs_it->second, config, output, use_string_literals);
}
} // namespace js2c
} // namespace node
Expand Down

0 comments on commit 80ed2c0

Please sign in to comment.