hermes-windows/test/hermes/external-mem.js

76 строки
2.0 KiB
JavaScript
Исходник Обычный вид История

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// RUN: %hermes -O -gc-max-heap=8M %s | %FileCheck --match-full-lines %s
print('ArrayBuffer')
// CHECK-LABEL: ArrayBuffer
// Smaller than max heap succeeds.
a = new ArrayBuffer(1000000);
// Even if done multiple times; GC should collect unused ones.
a = new ArrayBuffer(1000000);
a = new ArrayBuffer(1000000);
a = new ArrayBuffer(1000000);
a = new ArrayBuffer(1000000);
a = null;
// Larger than max heap fails
try {
a = new ArrayBuffer(12000000);
} catch (x) {
print(x)
// CHECK-NEXT: RangeError: Cannot allocate a data block for the ArrayBuffer
}
a = null;
print('ExternalStringPrimitive')
// CHECK-LABEL: ExternalStringPrimitive
buffered concatenation of string primitives Summary: Attempt to address the case where smaller strings are repeatedly appended to a larger string. Example code might look like this: var str = ""; for(let i = 0; i < 100000; ++i) str += " "; A naive execution of this code leads to quadratic behavior because it requires an allocation and a copy of the string for each iteration. Traditionally that is addressed by a tree-like data structure called a "rope", which represents each string addition by a new heap object with pointers to the "left" and "right" side of the addition. It can work well performance-wise, but it has significant memory overhead because it can require allocating an object per character. Additionally it introduces significant complexity when the "rope" needs to be "flattened" in order to be processed by traditional string functions. We have chosen an alternative approach designed in a discussion with David Detlefs and Riley Dulin. It relies on implicitly maintaining a growable "concatenation buffer" where every new string is appended to the buffer and the result of the concatenation is a new StringPrimitive referencing a prefix of the buffer. This approach can also produce a new object per appended character (the result StringPrimitive), but unlike ropes, that object is not part of a tree, so it potentially becomes garbage collectable very soon, usually in the next young-gen collection. There are two objects that represent this model at runtime: - the concatenation buffer containing storage which doubles its capacity when necessary. - BufferedStringPrimitive containing a reference to the concatenation buffer and a length. This represents the result of a single concatenation. The concatenation algorithm is very simple: 1. If the left part of the concatenation is already a BufferedStringPrimitive, get its concatenation buffer, append the right part to it, then allocate and return a new BufferedStringPrimitive with the same buffer and the new length. 2. Otherwise, if the combined length of the two strings is above a threshold, allocate a new buffer, initialize it with the the concatenation of the two strings and return a new BufferedStringPrimitive with the buffer and length. (This ignores some details like ASCII and UTF16 strings, which do not change the overall complexity.) This algorithm is very simple and it results in strings that are already flattened in memory, so they are usable immediately at any time. This commit makes use of a further simplification: this optimization primarily benefits "large" strings, which already live in native memory and a represented by a `std::basic_string<>`. A `std::basic_string<>` is already growable string storage and we even already have a heap object that wraps it: `ExternalStringPrimitive<>`. So we can literally use ExternalStringPrimitive<> to represent the concatenation buffer without almost any changes. There are several issues that need to be investigated more and possibly addressed in future commits: 1. Perhaps this optimizations is beneficial at smaller strings sizes too, which might mean that we also need a "in-GC-heap" representation of the concatenation buffer. 2. Since all concatenation steps share the same buffer, it is possible that an early step could keep alive in memory a significantly larger concatenation buffer. This is unlikely but possible. One way to address it would be to allocate a new concatenation buffer after some amount if resizing, this putting an upper limit on how much each step can keep in memory. 3. The std::basic_string<> excess capacity should probably be trimmed during full collections. The performance results are encouraging: pdfjs.js speeds up by a factor of about 3x. Reviewed By: mhorowitz Differential Revision: D17955802 fbshipit-source-id: 9f4c365e86337ced8e30828c4bd39664883bb06f
2019-10-28 22:12:36 +03:00
// Note: use Array.join() instead of string concatenation to avoid the relative
// unpredictability of fast buffered concatenation.
var s10 = 'aaaaaaaaaa';
buffered concatenation of string primitives Summary: Attempt to address the case where smaller strings are repeatedly appended to a larger string. Example code might look like this: var str = ""; for(let i = 0; i < 100000; ++i) str += " "; A naive execution of this code leads to quadratic behavior because it requires an allocation and a copy of the string for each iteration. Traditionally that is addressed by a tree-like data structure called a "rope", which represents each string addition by a new heap object with pointers to the "left" and "right" side of the addition. It can work well performance-wise, but it has significant memory overhead because it can require allocating an object per character. Additionally it introduces significant complexity when the "rope" needs to be "flattened" in order to be processed by traditional string functions. We have chosen an alternative approach designed in a discussion with David Detlefs and Riley Dulin. It relies on implicitly maintaining a growable "concatenation buffer" where every new string is appended to the buffer and the result of the concatenation is a new StringPrimitive referencing a prefix of the buffer. This approach can also produce a new object per appended character (the result StringPrimitive), but unlike ropes, that object is not part of a tree, so it potentially becomes garbage collectable very soon, usually in the next young-gen collection. There are two objects that represent this model at runtime: - the concatenation buffer containing storage which doubles its capacity when necessary. - BufferedStringPrimitive containing a reference to the concatenation buffer and a length. This represents the result of a single concatenation. The concatenation algorithm is very simple: 1. If the left part of the concatenation is already a BufferedStringPrimitive, get its concatenation buffer, append the right part to it, then allocate and return a new BufferedStringPrimitive with the same buffer and the new length. 2. Otherwise, if the combined length of the two strings is above a threshold, allocate a new buffer, initialize it with the the concatenation of the two strings and return a new BufferedStringPrimitive with the buffer and length. (This ignores some details like ASCII and UTF16 strings, which do not change the overall complexity.) This algorithm is very simple and it results in strings that are already flattened in memory, so they are usable immediately at any time. This commit makes use of a further simplification: this optimization primarily benefits "large" strings, which already live in native memory and a represented by a `std::basic_string<>`. A `std::basic_string<>` is already growable string storage and we even already have a heap object that wraps it: `ExternalStringPrimitive<>`. So we can literally use ExternalStringPrimitive<> to represent the concatenation buffer without almost any changes. There are several issues that need to be investigated more and possibly addressed in future commits: 1. Perhaps this optimizations is beneficial at smaller strings sizes too, which might mean that we also need a "in-GC-heap" representation of the concatenation buffer. 2. Since all concatenation steps share the same buffer, it is possible that an early step could keep alive in memory a significantly larger concatenation buffer. This is unlikely but possible. One way to address it would be to allocate a new concatenation buffer after some amount if resizing, this putting an upper limit on how much each step can keep in memory. 3. The std::basic_string<> excess capacity should probably be trimmed during full collections. The performance results are encouraging: pdfjs.js speeds up by a factor of about 3x. Reviewed By: mhorowitz Differential Revision: D17955802 fbshipit-source-id: 9f4c365e86337ced8e30828c4bd39664883bb06f
2019-10-28 22:12:36 +03:00
var s100 = [s10, s10, s10, s10, s10, s10, s10, s10, s10, s10].join("")
function strOfSize(n) {
if (n < 100) {
return s100.substring(0, n);
} else {
var leftSize = Math.floor(n / 2);
var left = strOfSize(leftSize);
// Don't allocate a string for rightSize -- that would mean exponential work,
// and live memory proportional to n before the final allocation.
if (leftSize * 2 == n) {
buffered concatenation of string primitives Summary: Attempt to address the case where smaller strings are repeatedly appended to a larger string. Example code might look like this: var str = ""; for(let i = 0; i < 100000; ++i) str += " "; A naive execution of this code leads to quadratic behavior because it requires an allocation and a copy of the string for each iteration. Traditionally that is addressed by a tree-like data structure called a "rope", which represents each string addition by a new heap object with pointers to the "left" and "right" side of the addition. It can work well performance-wise, but it has significant memory overhead because it can require allocating an object per character. Additionally it introduces significant complexity when the "rope" needs to be "flattened" in order to be processed by traditional string functions. We have chosen an alternative approach designed in a discussion with David Detlefs and Riley Dulin. It relies on implicitly maintaining a growable "concatenation buffer" where every new string is appended to the buffer and the result of the concatenation is a new StringPrimitive referencing a prefix of the buffer. This approach can also produce a new object per appended character (the result StringPrimitive), but unlike ropes, that object is not part of a tree, so it potentially becomes garbage collectable very soon, usually in the next young-gen collection. There are two objects that represent this model at runtime: - the concatenation buffer containing storage which doubles its capacity when necessary. - BufferedStringPrimitive containing a reference to the concatenation buffer and a length. This represents the result of a single concatenation. The concatenation algorithm is very simple: 1. If the left part of the concatenation is already a BufferedStringPrimitive, get its concatenation buffer, append the right part to it, then allocate and return a new BufferedStringPrimitive with the same buffer and the new length. 2. Otherwise, if the combined length of the two strings is above a threshold, allocate a new buffer, initialize it with the the concatenation of the two strings and return a new BufferedStringPrimitive with the buffer and length. (This ignores some details like ASCII and UTF16 strings, which do not change the overall complexity.) This algorithm is very simple and it results in strings that are already flattened in memory, so they are usable immediately at any time. This commit makes use of a further simplification: this optimization primarily benefits "large" strings, which already live in native memory and a represented by a `std::basic_string<>`. A `std::basic_string<>` is already growable string storage and we even already have a heap object that wraps it: `ExternalStringPrimitive<>`. So we can literally use ExternalStringPrimitive<> to represent the concatenation buffer without almost any changes. There are several issues that need to be investigated more and possibly addressed in future commits: 1. Perhaps this optimizations is beneficial at smaller strings sizes too, which might mean that we also need a "in-GC-heap" representation of the concatenation buffer. 2. Since all concatenation steps share the same buffer, it is possible that an early step could keep alive in memory a significantly larger concatenation buffer. This is unlikely but possible. One way to address it would be to allocate a new concatenation buffer after some amount if resizing, this putting an upper limit on how much each step can keep in memory. 3. The std::basic_string<> excess capacity should probably be trimmed during full collections. The performance results are encouraging: pdfjs.js speeds up by a factor of about 3x. Reviewed By: mhorowitz Differential Revision: D17955802 fbshipit-source-id: 9f4c365e86337ced8e30828c4bd39664883bb06f
2019-10-28 22:12:36 +03:00
return [left, left].join("");
} else {
// n == 2 * leftSize + 1:
buffered concatenation of string primitives Summary: Attempt to address the case where smaller strings are repeatedly appended to a larger string. Example code might look like this: var str = ""; for(let i = 0; i < 100000; ++i) str += " "; A naive execution of this code leads to quadratic behavior because it requires an allocation and a copy of the string for each iteration. Traditionally that is addressed by a tree-like data structure called a "rope", which represents each string addition by a new heap object with pointers to the "left" and "right" side of the addition. It can work well performance-wise, but it has significant memory overhead because it can require allocating an object per character. Additionally it introduces significant complexity when the "rope" needs to be "flattened" in order to be processed by traditional string functions. We have chosen an alternative approach designed in a discussion with David Detlefs and Riley Dulin. It relies on implicitly maintaining a growable "concatenation buffer" where every new string is appended to the buffer and the result of the concatenation is a new StringPrimitive referencing a prefix of the buffer. This approach can also produce a new object per appended character (the result StringPrimitive), but unlike ropes, that object is not part of a tree, so it potentially becomes garbage collectable very soon, usually in the next young-gen collection. There are two objects that represent this model at runtime: - the concatenation buffer containing storage which doubles its capacity when necessary. - BufferedStringPrimitive containing a reference to the concatenation buffer and a length. This represents the result of a single concatenation. The concatenation algorithm is very simple: 1. If the left part of the concatenation is already a BufferedStringPrimitive, get its concatenation buffer, append the right part to it, then allocate and return a new BufferedStringPrimitive with the same buffer and the new length. 2. Otherwise, if the combined length of the two strings is above a threshold, allocate a new buffer, initialize it with the the concatenation of the two strings and return a new BufferedStringPrimitive with the buffer and length. (This ignores some details like ASCII and UTF16 strings, which do not change the overall complexity.) This algorithm is very simple and it results in strings that are already flattened in memory, so they are usable immediately at any time. This commit makes use of a further simplification: this optimization primarily benefits "large" strings, which already live in native memory and a represented by a `std::basic_string<>`. A `std::basic_string<>` is already growable string storage and we even already have a heap object that wraps it: `ExternalStringPrimitive<>`. So we can literally use ExternalStringPrimitive<> to represent the concatenation buffer without almost any changes. There are several issues that need to be investigated more and possibly addressed in future commits: 1. Perhaps this optimizations is beneficial at smaller strings sizes too, which might mean that we also need a "in-GC-heap" representation of the concatenation buffer. 2. Since all concatenation steps share the same buffer, it is possible that an early step could keep alive in memory a significantly larger concatenation buffer. This is unlikely but possible. One way to address it would be to allocate a new concatenation buffer after some amount if resizing, this putting an upper limit on how much each step can keep in memory. 3. The std::basic_string<> excess capacity should probably be trimmed during full collections. The performance results are encouraging: pdfjs.js speeds up by a factor of about 3x. Reviewed By: mhorowitz Differential Revision: D17955802 fbshipit-source-id: 9f4c365e86337ced8e30828c4bd39664883bb06f
2019-10-28 22:12:36 +03:00
return [left, left, 'a'].join("");
}
}
}
// Smaller than max heap succeeds.
var s = strOfSize(1000000);
// Even if done multiple times; GC should collect unused ones.
s = strOfSize(1000000);
s = strOfSize(1000000);
s = strOfSize(1000000);
s = strOfSize(1000000);
s = null;
// Larger than max heap fails.
try {
s = strOfSize(6000000);
print('no exception');
} catch (x) {
print(x)
// CHECK-NEXT: RangeError: Cannot allocate an external string primitive.
}