ES2022のArray.prototype.atは今の所遅い (2021/10/16時点)

# Array.prototype.atは何ができるの

Pythonのように, 負数を指定すると配列の後ろから取得できます. 特に最後尾の数個や, 文字列での末尾からの指定など, 待ち望まれていた機能です. (String.prototype.atもあります)

```js
const arr = [1, 2, 3]
console.log(arr.at(-1)); // 3
```

## 速度

普通の添字で指定と速度を比べてみます.

バージョンは

```text
> node -v
v16.9.0
```

ソース

```js
const arr = [...Array(1000*1000)].map((_, i) => i);
// for内の余分な計算は取っ払いたいので先に
const len = arr.length;
const len2 = len - 1;
const nlen = -len;

// [] 昇順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = 0; i < len; ++i) {
            arr[i];
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`インデックス昇順: ${endTime - startTime}ms`);
}

// [] 降順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = len2; i >= 0; --i) {
            arr[i];
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`インデックス降順 ${endTime - startTime}ms`);
}

// at 昇順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = 0; i < len; ++i) {
            arr.at(i);
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`at昇順 ${endTime - startTime}ms`);
}

// at 降順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = -1; i >= nlen; --i) {
            arr.at(i);
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`at降順 ${endTime - startTime}ms`);
}
```

結果

```text
インデックス昇順: 47ms
インデックス降順 82ms
at昇順 4189ms
at降順 4462ms
```

びっくりするほど遅いです.

Chromeでも大体同じ傾向だったため, Nodejsの実装が特別悪いというわけでもなさそうです.

## polyfillのほうが大幅に速い?

https://github.com/tc39/proposal-relative-indexing-method#polyfill

からコードを持ってきて, 簡易的に試してみます.

```js
Array.prototype.myat = function (n) {
	// ToInteger() abstract op
	n = Math.trunc(n) || 0;
	// Allow negative indexing from the end
	if (n < 0) n += this.length;
	// OOB access is guaranteed to return undefined
	if (n < 0 || n >= this.length) return undefined;
	// Otherwise, this is just normal property access
	return this[n];
}

// myat 昇順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = 0; i < len; ++i) {
            arr.myat(i);
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`myat昇順 ${endTime - startTime}ms`);
}

// myat 降順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = -1; i >= mlen; --i) {
            arr.myat(i);
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`myat降順 ${endTime - startTime}ms`);
}
```

```text
myat昇順 139ms
myat降順 101ms
```

速いです. 一瞬バグではないかと思いましたが, 添字にNaNや文字列を入れたりしても挙動は合っていたので間違いではないようです. ループ数も100で合っています.

最初は遅い理由を関数のオーバーヘッドガーや単純に処理が多いからーなどと考えていましたが, そう簡単ではないようです. この先は実装を解明してもらえる方に… ﾁﾗｯ

現状組み込みのArray.prototype.atがびっくりするくらい遅いだけで, 本来は添字に近い性能が出るのではないかと思います. Webアプリ開発における普段使いでは大きな影響はないと思いますが, 将来的によく使う機能になると思うので今後の性能改善に期待です.

<details><summary>全体ソース</summary><div>


```js
const arr = [...Array(1000*1000)].map((_, i) => i);
const len = arr.length;
const len2 = len - 1;
const nlen = -len;

Array.prototype.myat = function (n) {
	// ToInteger() abstract op
	n = Math.trunc(n) || 0;
	// Allow negative indexing from the end
	if (n < 0) n += this.length;
	// OOB access is guaranteed to return undefined
	if (n < 0 || n >= this.length) return undefined;
	// Otherwise, this is just normal property access
	return this[n];
}

// [] 昇順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = 0; i < len; ++i) {
            arr[i];
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`インデックス昇順: ${endTime - startTime}ms`);
}

// [] 降順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = len2; i > 0; --i) {
            arr[i];
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`インデックス降順 ${endTime - startTime}ms`);
}

// at 昇順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = 0; i < len; ++i) {
            arr.at(i);
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`at昇順 ${endTime - startTime}ms`);
}

// at 降順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = -1; i >= nlen; --i) {
            arr.at(i);
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`at降順 ${endTime - startTime}ms`);
}

// myat 昇順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = 0; i < len; ++i) {
            arr.myat(i);
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`myat昇順 ${endTime - startTime}ms`);
}

// myat 降順
{
    const startTime = (new Date()).getTime();
    for (let n = 0; n < 100; ++n) {
        for (let i = -1; i >= nlen; --i) {
            arr.myat(i);
        }
    }
    const endTime = (new Date()).getTime();
    console.log(`myat降順 ${endTime - startTime}ms`);
}
```

</div></details>