Skip to content

Commit e621ea9

Browse files
authored
Merge pull request #27 from bldl/optimizing-section
imported updated version of optimization-section
2 parents 776005b + da17972 commit e621ea9

File tree

1 file changed

+102
-90
lines changed

1 file changed

+102
-90
lines changed

upsert-tutorial/index.html

Lines changed: 102 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ <h2>What&#39;s Covered in This Tutorial?</h2>
8484

8585
<!-- Collapsible Sections for Detailed Topics -->
8686
<section class="collapsible" id="proposal-overview">
87-
<summary><h2>The `Map.prototype.upsert` proposal</h2></summary>
87+
<h2>The `Map.prototype.upsert` proposal</h2>
8888
<p><strong>What is it?</strong></p>
8989
<p><code>Map.prototype.upsert</code> is a new method for <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map"><code>Map</code>-object</a> in JavaScript™. The operation simplifies the process of inserting or updating key-value pairs in the <code>Map</code>: it checks for existence of a key and then either <code>insert</code>s or <code>update</code>s a key-value pair. </p>
9090
<p>This proposal is also intended for <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap"><code>WeakMap</code></a>, which shares a similar API structure. The behavior of <code>upsert</code> in <code>WeakMap</code> would follow the same principles, with the primary difference being the behavior of keys in <code>WeakMap</code>, as they are held weakly and can be garbage collected when no other references to them exist. In this tutorial however, we will focus on the <code>Map</code>-implementation.</p>
@@ -1318,26 +1318,29 @@ <h2>Writing the Specification in Ecmarkup</h2>
13181318
<section class="collapsible" id="optimization">
13191319
<h2>Optimization</h2>
13201320
<div class="content-body">
1321-
<p>A proposal goes through several <a href="https://www.proposals.es/stages" target="_blank">stages</a> before it becomes a part of the ECMAScript® language.
1322-
Every new feature introduces complexity, which can affect the performance of the SpiderMonkey engine.
1323-
Therefore optimization becomes crucial when designing and implementing these features.
1324-
In our case there is especially one line which could use some optimization:</p>
1325-
<pre><code>4. For each Record { [[Key]], [[Value]] } e that is an element of entries, do</code></pre>
1326-
<p> As of right now it is implemented like this:</p>
1327-
<pre><code class="language-js">for (var e of allowContentIter(entries)) {
1321+
<p>When a new feature is being added to the language, we must consider the complexity it introduces - this can affect the performance of the SpiderMonkey engine.
1322+
Therefore, optimization becomes crucial when designing and implementing proposals.
1323+
In our case, step 4 of the specification </p>
1324+
<pre><code class="language-lua">4. For each Record { [[Key]], [[Value]] } e that is an element of entries, do
1325+
</code></pre>
1326+
<p> could use some optimization.</p>
1327+
<p> Currently, this step is implemented like this:</p>
1328+
<pre><code class="language-js">// 4. For each Record { [[Key]], [[Value]] } e that is an element of entries, do
1329+
for (var e of allowContentIter(entries)) {
13281330
var eKey = e[0];
13291331
var eValue = e[1];
1330-
//...
1332+
// ...
13311333
}
13321334
</code></pre>
1333-
<p> The worst case for this is that is loops through the entire <code>entries</code>. The result is a runtime of <strong><code>O(n)</code></strong> where <code>n</code> is the size of the <code>Map</code>.
1334-
This is rather slow, considering a lookup in maps should be <strong><code>~O(1)</code></strong>, given an efficient <code>HashTable</code> implementation.
1335-
Therefore, we decided to try optimizing this line.</p>
1336-
<p> <strong>Demonstration: Create a new file; Runtime.js with the code below and run the script with <code>./mach build</code> and <code>./mach run Runtime.js</code></strong></p>
1337-
<details>
1338-
<summary>Runtime script</summary>
1339-
1340-
<pre><code class="language-js"> const iterations = 1000;
1335+
<p> In the worst case scenario, the loop would go through all of the <code>entries</code>, resulting in linear time complexity (that is, <code>O(n)</code> where <code>n</code> is the size of the <code>Map</code>).
1336+
This is rather slow, especially considering that a lookup in maps could be done in a constant time (<code>~O(1)</code>), given an efficient <code>HashTable</code> implementation.
1337+
In this section, we use this fact to optimize the implementation of the step 4 in the specification.</p>
1338+
<p> Before proceeding, we informally demonstrate the performance of the current design of <code>upsert</code>.
1339+
In the code below, we measure the runtime of of updating or inserting key-value pairs into a <code>Map</code> object:
1340+
using the <code>upsert</code> method vs. using <code>has</code>, <code>get</code>, and <code>set</code>.
1341+
The <code>measureRuntime</code> function is used to execute and log the execution time of each approach over a fixed number of iterations.</p>
1342+
<p> We can create a new file <code>Runtime.js</code> with the code below and run it with: <code>./mach build</code> and <code>./mach run Runtime.js</code>.</p>
1343+
<pre><code class="language-js">const iterations = 1000;
13411344

13421345
// Function to measure runtime of a given block of code
13431346
function measureRuntime(callback, description) {
@@ -1353,7 +1356,7 @@ <h2>Optimization</h2>
13531356
console.log(`Runtime: ${runtime} milliseconds \n`);
13541357
}
13551358

1356-
// test upsert for e record of entries
1359+
// test `upsert` for e record of entries
13571360
function withUpsert() {
13581361
const m = new Map();
13591362

@@ -1364,7 +1367,7 @@ <h2>Optimization</h2>
13641367
}
13651368
}
13661369

1367-
//test without upsert
1370+
// test without `upsert`
13681371
function withoutUpsert() {
13691372
const m = new Map();
13701373

@@ -1379,109 +1382,118 @@ <h2>Optimization</h2>
13791382
}
13801383
}
13811384

1382-
console.log(&quot;Starting tests...&quot;);
1383-
measureRuntime(withUpsert, &quot;Test upsert for &quot; + iterations + &quot; iterations&quot;);
1384-
measureRuntime(withoutUpsert, &quot;Test without upsert for &quot; + iterations + &quot; iterations&quot;);
1385-
</code></pre>
1386-
</details>
1387-
1388-
1389-
<p> One solution we had, was to check if the entry was in the <code>Map</code>, by using <code>Map::has</code>.
1390-
The problem with this, is that this method is not currently exposed to self-hosted JavaScript™ code. The reason for this
1391-
is seemingly because there has not been any need for the <code>Map::has</code> method in self-hosted code previously.</p>
1392-
<p> <strong>Exposing <code>std_Map_has</code> to self-hosted code</strong></p>
1393-
<p> <code>Selfhosting.cpp</code></p>
1394-
<pre><code class="language-cpp">
1385+
console.log(&quot;Starting tests ...&quot;);
1386+
measureRuntime(withUpsert, &quot;Test `upsert` for &quot; + iterations + &quot; iterations&quot;);
1387+
measureRuntime(withoutUpsert, &quot;Test without `upsert` for &quot; + iterations + &quot; iterations&quot;);
1388+
</code></pre>
1389+
<p> Now we are ready to consider how the step 4 in the specification can be implemented in a more optimal way.
1390+
One solution we could have is to check if an entry was in the <code>Map</code> using the function <a href="https://262.ecma-international.org/#sec-map.prototype.has"><code>Map::has</code></a>.
1391+
The problem with this solution, however, is that this method is not currently exposed to self-hosted JavaScript™ code.
1392+
An apparent reason for this is that there has not been any need for the <code>Map::has</code> method in self-hosted code previously.</p>
1393+
<p> Recall the <code>Map</code> methods exposed to self-hosted code in <a href="https://searchfox.org/mozilla-central/source/js/src/vm/SelfHosting.cpp#2383"><code>SelfHosting.cpp</code></a>:</p>
1394+
<pre><code class="language-cpp"> // This code is from: /js/src/vm/SelfHosting.cpp
1395+
// ...
13951396
// Standard builtins used by self-hosting.
1396-
//...
1397+
// ...
13971398
JS_FN(&quot;std_Map_entries&quot;, MapObject::entries, 0, 0),
13981399
JS_FN(&quot;std_Map_get&quot;, MapObject::get, 1, 0),
13991400
JS_FN(&quot;std_Map_set&quot;, MapObject::set, 2, 0),
1400-
//...
1401+
// ...
14011402
</code></pre>
1402-
<p> We want to add this line so that we can use <code>has</code> in our optimized implementation.</p>
1403-
<pre><code class="language-cpp">
1404-
JS_INLINABLE_FN(&quot;std_Map_has&quot;, MapObject::has, 1, 0, MapHas),
1403+
<p> We can expose <code>std_Map_has</code> to self-hosted code by adding the following line in <code>SelfHosting.cpp</code>:</p>
1404+
<pre><code class="language-cpp"> JS_INLINABLE_FN(&quot;std_Map_has&quot;, MapObject::has, 1, 0, MapHas),
14051405
</code></pre>
1406-
<p> Copy the line and paste it into <code>js/src/vm/Selfhosting.cpp</code>, before <code>MapObject::set</code> (to ensure consistency across files).</p>
1407-
<details>
1408-
<summary>Solution</summary>
1409-
1410-
<pre><code class="language-cpp">
1411-
// Standard builtins used by self-hosting.
1412-
//...
1413-
JS_FN(&quot;std_Map_entries&quot;, MapObject::entries, 0, 0),
1414-
JS_FN(&quot;std_Map_get&quot;, MapObject::get, 1, 0),
1415-
JS_INLINABLE_FN(&quot;std_Map_has&quot;, MapObject::has, 1, 0, MapHas),
1416-
JS_FN(&quot;std_Map_set&quot;, MapObject::set, 2, 0),
1417-
//...
1406+
<p>To ensure consistency across files, we add this line before <code>JS_FN(&quot;std_Map_set&quot;, MapObject::set, 2, 0),</code>, resulting in the following:</p>
1407+
<pre><code class="language-cpp"> // This code is from: /js/src/vm/SelfHosting.cpp
1408+
// ...
1409+
// Standard builtins used by self-hosting.
1410+
// ...
1411+
JS_FN(&quot;std_Map_entries&quot;, MapObject::entries, 0, 0),
1412+
JS_FN(&quot;std_Map_get&quot;, MapObject::get, 1, 0),
1413+
JS_INLINABLE_FN(&quot;std_Map_has&quot;, MapObject::has, 1, 0, MapHas), // we have added this line
1414+
JS_FN(&quot;std_Map_set&quot;, MapObject::set, 2, 0),
1415+
// ...
14181416
</code></pre>
1419-
</details>
1420-
1421-
<p> We also need to make the <code>has</code> method publicly exposed in <code>MapObject.h</code> to use it in self-hosted code.</p>
1422-
<p> In <code>MapObject.h</code>, move this line from <strong>private</strong> to <strong>public</strong>.</p>
1417+
<p> We also need to make the method <code>has</code> publicly exposed in <a href="https://searchfox.org/mozilla-central/source/js/src/builtin/MapObject.h"><code>MapObject.h</code></a>.
1418+
To do this, we replace the visibility modifier of <a href="https://searchfox.org/mozilla-central/source/js/src/builtin/MapObject.h#217">the method with signature</a> </p>
14231419
<pre><code class="language-cpp">[[nodiscard]] static bool has(JSContext* cx, unsigned argc, Value* vp);
14241420
</code></pre>
1425-
<details>
1426-
<summary>(This could be tricky) Let's break down the structure of the file: </summary>
1427-
1428-
<pre><code class="language-cpp"> class MapObject : public NativeObject {
1421+
<p> from <a href="https://searchfox.org/mozilla-central/source/js/src/builtin/MapObject.h#186"><code>private</code></a> to <a href="https://searchfox.org/mozilla-central/source/js/src/builtin/MapObject.h#109"><code>public</code></a>.</p>
1422+
<pre><code class="language-cpp"> // This code is from: /js/src/builtin/MapObject.h
1423+
// ...
1424+
class MapObject : public NativeObject {
14291425
public:
1430-
//...
1426+
// ...
14311427
const ValueMap* getData() { return getTableUnchecked(); }
14321428

14331429
[[nodiscard]] static bool get(JSContext* cx, unsigned argc, Value* vp);
14341430
[[nodiscard]] static bool set(JSContext* cx, unsigned argc, Value* vp);
1435-
//add has here
1431+
// add `has` here
14361432

1437-
//...
1433+
// ...
14381434

14391435
private:
1440-
//...
1436+
// ...
14411437
[[nodiscard]] static bool size_impl(JSContext* cx, const CallArgs&amp; args);
14421438
[[nodiscard]] static bool size(JSContext* cx, unsigned argc, Value* vp);
14431439
[[nodiscard]] static bool get_impl(JSContext* cx, const CallArgs&amp; args);
14441440
[[nodiscard]] static bool has_impl(JSContext* cx, const CallArgs&amp; args);
1445-
[[nodiscard]] static bool has(JSContext* cx, unsigned argc, Value* vp); //remove this line
1441+
[[nodiscard]] static bool has(JSContext* cx, unsigned argc, Value* vp); // remove this line
14461442
[[nodiscard]] static bool set_impl(JSContext* cx, const CallArgs&amp; args);
14471443
[[nodiscard]] static bool delete_impl(JSContext* cx, const CallArgs&amp; args);
14481444
[[nodiscard]] static bool delete_(JSContext* cx, unsigned argc, Value* vp);
14491445
[[nodiscard]] static bool keys_impl(JSContext* cx, const CallArgs&amp; args);
1450-
//...
1446+
// ...
14511447
}
14521448
</code></pre>
1453-
</details>
1449+
<p>This will enable us to use <a href="https://262.ecma-international.org/#sec-map.prototype.has"><code>has</code></a> in our optimized implementation: in self-hosted JavaScript™, we will be able to call this method using <code>callFunction</code> and passing <code>std_Map_has</code> as an argument.</p>
1450+
<h3 id="optimizing-the-implementation-of-upsert">Optimizing the implementation of <code>upsert</code></h3>
1451+
<p> We can now modify our implementation of the <code>upsert</code> method to use <code>std_Map_has</code> instead of a <em><code>for ... of</code></em> loop and <a href="https://262.ecma-international.org/#sec-samevaluezero"><code>SameValueZero</code></a>.</p>
1452+
<pre><code class="language-js">function MapUpsert(key, value) {
1453+
// 1. Let M be the this value.
1454+
var M = this;
14541455

1455-
<p> The <code>std_Map_has</code> method should now be available in self-hosted JavaScript™.</p>
1456-
<h3 id="optimize-the-function">Optimize the function</h3>
1457-
<p> With <code>has</code> now exposed to self-hosted code, alter your implementation to use <code>std_Map_has</code> instead of a <code>for-of</code> loop
1458-
and <code>SameValueZero</code>.</p>
1459-
<details>
1460-
<summary>Solution</summary>
1456+
// 2. Perform ? RequireInternalSlot(M, [[MapData]]).
1457+
if (!IsObject(M) || (M = GuardToMapObject(M)) === null) {
1458+
return callFunction(
1459+
CallMapMethodIfWrapped,
1460+
this,
1461+
key,
1462+
value,
1463+
&quot;MapUpsert&quot;
1464+
);
1465+
}
14611466

1462-
<pre><code class="language-js">
1463-
function MapUpsert(key, value) {
1464-
var M = this;
1465-
1466-
if (!IsObject(M) || (M = GuardToMapObject(M)) === null) {
1467-
return callFunction(
1468-
CallMapMethodIfWrapped,
1469-
this,
1470-
key,
1471-
value,
1472-
&quot;MapUpsert&quot;
1473-
);
1474-
}
1467+
// 3. Let entries be the List that is M.[[MapData]].
1468+
// 4. For each Record { [[Key]], [[Value]] } e that is an element of entries, do
1469+
// 4a. If e.[[Key]] is not empty and SameValueZero(e.[[Key]], key) is true, return e.[[Value]].
1470+
if (callFunction(std_Map_has, M, key)) {
1471+
return callFunction(std_Map_get, M, key);
1472+
}
14751473

1476-
if (callFunction(std_Map_has, M, key)) {
1477-
return callFunction(std_Map_get, M, key);
1478-
}
1474+
// 5. Set e.[[Value]] to value.
1475+
callFunction(std_Map_set, M, key, value);
14791476

1480-
callFunction(std_Map_set, M, key, value);
1481-
return value;
1482-
}
1477+
// 6. Return e.[[Value]].
1478+
return value;
1479+
}
14831480
</code></pre>
1484-
</details>
1481+
<p>It is important to note that now our implementation does not follow the exact structure of the <code>upsert</code> specification text.
1482+
This is a common practice - as long as we make sure that the produced
1483+
<a href="https://github.com/tc39/how-we-work/blame/cc47a79340a773876cb03371dc2d46b9d9ce9695/how-to-read.md#L7"><strong>observable behavior</strong></a>
1484+
is the same as the one defined by the spec.
1485+
This ensures compatibility and correctness while allowing flexibility in optimization or internal design.</p>
1486+
<p>We quote here a <a href="https://github.com/tc39/how-we-work/blame/cc47a79340a773876cb03371dc2d46b9d9ce9695/how-to-read.md#L7">comment</a> on the <a href="https://github.com/tc39/how-we-work"><em>How We Work</em></a> document by TC39:</p>
1487+
<blockquote>
1488+
<p><em>Specification text is meant to be interpreted abstractly. Only the observable semantics, that is, the behavior when JavaScript™ code is executed, need to match the specification. Implementations can use any strategy they want to make that happen, including using different algorithms that reach the same result.</em></p>
1489+
</blockquote>
1490+
<p>Further discussion on this can be found <a href="https://github.com/tc39/how-we-work/issues/104">here</a>.</p>
1491+
<p>Note also that we don&#39;t need to change the specification text to reflect our optimized implementation:
1492+
the specification is intended to define behavior at an abstract level, providing a precise but implementation-agnostic guide for JavaScript™ engines.
1493+
This abstraction ensures that the specification remains broadly applicable,
1494+
leaving room for diverse implementations while guaranteeing consistent observable behavior across compliant engines.
1495+
</p>
1496+
14851497
</div>
14861498
</section>
14871499

0 commit comments

Comments
 (0)