Numbaを用いたPython/Numpyの高速化

f:id:k-kty:20190216135709p:plain

まえがき

機械学習関連のプロジェクトではPythonを使う人が多いと思うのですが、Pythonでの処理って遅いですよね。

Tensorflowでの学習などにおいてはPython側で計算をしているわけではないので大丈夫なのですが、前処理の際などにPythonの実行速度の遅さがとても気になります。コンパイラ言語とスクリプト言語を比べるのは酷ですが、「C++なら数秒で終わるのになあ」という処理も数分かかってしまったりします。

もちろんC++のプログラムに匹敵するほどの速度を出すのは無理ですが、できるだけ高速化したいですよね。 そのための方法はいろいろあるのですが、今回はNumbaというJITコンパイラを用いる方法を紹介したいと思います。

Numbaとは

公式サイトによると、

Numba is an open source JIT compiler that translates a subset of Python and NumPy code into fast machine code.

「Python/Numpyコード(の一部)を、高速な機械語に変換するためのオープンソースJITコンパイラ」ってことみたいですね。

もう少し細かい特徴としては以下のような感じです。

  • 関数にデコレータをつけるだけで動作する
  • Numpyの配列や関数を用いた処理に特化している
  • ランタイム時に、最適化された機械語を作成して実行してくれる
  • Numbaを利用するためにPythonのインタープリターを変えたり、コードの実行前にコンパイルするようにしたりする必要がない
  • CPU/GPUを用いた並列計算のための機能もある

実際に使ってみる

高速化前のコード

以下のような関数があったとします。 配列(arrとします)を受け取り、[log(arr[1]/arr[0]), log(arr[2]/arr[1])... ] のような(長さがarrより1短い)配列を返します。

def calculate_diffs(arr):
  ret = numpy.empty(arr.size - 1)
  for i in range(arr.size - 1):
    ret[i] = numpy.log(arr[i + 1] / arr[i])
  return ret

以下のコードを用いて測定をすると(かなり雑ですが、今回はだいたいのパフォーマンスの比較ということで...)、出力は 183.20000410079956 となりました。約3分です。 なお関数内のnumpy.empty(...)の部分は計算とは関係ありませんがe-5秒くらいのオーダーだったので無視してよさそうです。(ちなみにですがnumpy.random.rand(...)の部分は無視できないほど時間がかかります。)

import time
import numpy

# 以上の関数定義

if __name__ == '__main__':
  arr = numpy.random.rand(10 ** 8)

  start_t = time.time()
  calculate_diffs(arr)
  end_t = time.time()

  print(end_t - start_t)

JITを有効化してみる

以下のようにデコレータをつけて、JITを有効化してみます。

@numba.jit(nopython=True)
def calculate_diffs(arr):
  ret = numpy.empty(arr.size - 1)
  for i in range(arr.size - 1):
    ret[i] = numpy.log(arr[i + 1] / arr[i])
  return ret

測定の結果、出力は1.805896282196045となり、100倍ほどの高速化に成功しました。

JITを有効化/並列化してみる

以下のようにデコレータを変更し、range関数をnumba.prange関数に置き換え、並列化してみます。 ret = numpy.empty(arr.size - 1)の部分は、numbaが勝手にうまいこと(hoist)してくれて、期待通りの動作をします。

@numba.jit(nopython=True, parallel=True)
def calculate_diffs(arr):
  ret = numpy.empty(arr.size - 1)
  for i in numba.prange(arr.size - 1):
    ret[i] = numpy.log(arr[i + 1] / arr[i])
  return ret

測定の結果、出力は0.7335679531097412となり、200倍以上の高速化に成功したことになります。

まとめ

限られたケースにはなりますが、Numbaをうまく使えば非常に簡単に高速化ができることがわかりました。

Express/Node.jsでTypeScriptを使ってみる

f:id:k-kty:20190213160011p:plain

最近、バックエンド開発でNode.jsを用いています。 JavaScriptは仕様がどんどん良くなってきているし、コールバックを用いた非同期処理も個人的には好きです。

しかしコードベースが大きくなると「型があればなあ...」と思うこともしばしば。 そこでTypeScriptに足を突っ込んでみることにしました。

そのときの記録の一部として、TypeScript + Node.js/ExpressでWebバックエンドの開発を始める方法を紹介します。

TypeScriptの文法・言語の機能についてはあまり触れません。(それらについては以下が参考になります。)

準備

npmプロジェクトを作成する

npm init を用いるなどして、package.jsonを作成します。

TypeScriptをインストール

$ npm i typescript --save-dev

JavaScriptからTypeScriptへのコンパイルに必要です。

TypeScriptの設定ファイルを作成

以下のような内容をtsconfig.jsonという名前で保存します。

{
  "compilerOptions": {
    "target": "es5",
    "outDir": "dist",
    "strict": true
  },
  "include": [
    "src/**/*",
  ]
}

軽く説明すると、このようになります。

  • target で、「どの仕様に対応したJavaScriptのコードをコンパイルによって生成するか」を指定しています。ここではES5を指定しています。
  • outDir で、コンパイル後のコードを保存するディレクトリを指定します。
  • strict をtrueにすることで、いろいろなTypeScriptのチェック機構が有効になります。
  • include で、コンパイルの対象にしたいファイル群を指定します。

expressとその「型定義ファイル」をインストール

$ npm i express --save
$ npm i @types/express --save-dev

このようにパッケージと一緒に型定義ファイルもインストールすることで、そのパッケージの関数やオブジェクトを使うときにもTypeScriptの型チェックを利用できるようになります。

ts-nodeをインストール

$ npm i ts-node --save-dev

これにより、node コマンドでJavaScriptコードを実行できるように、ts-node コマンドでTypeScriptコードを実行できるようになります。 ts-nodeは、「tscを用いてTypeScriptのコードをJavaScriptへコンパイルし、それをnodeで実行する」ということをしてくれています。 開発中に使うと便利です。なお、本番環境での仕様は推奨されていません。

実行スクリプトを追加

package.jsonの scripts の中身を以下のように書き換えます。

{
  "build": "tsc",
  "start": "node dist/index.js",
  "dev": "ts-node src/index.ts"
}

npm run build でTypeScriptのコードをコンパイルし、npm run start でそのJavaScriptコードを実行できるようになります。 npm run dev はその2つを組み合わせたことをやってくれますが、開発中のみ利用します。

Hello, Worldしてみる

コード

src/index.ts

import app from './app';

app.listen(process.env.PORT || 3000);

src/app.ts

import express from 'express';
import routes from './routes';

const app = express();

app.use('/', routes);

export default app;

src/routes.ts

import express from 'express';

const router = express.Router();

router.get('/', (req, res, next) => {
  res.send('Hello, World');
});

export default router;

起動

以下のコマンドでサーバーが起動します。

$ npm run dev

以下のように、きちんと動いていることがわかります。

$ curl http://localhost:3000/
Hello, World

テストをする

supertest を用いて、エンドポイント単位のテストをしてみます。

準備

supertestのインストール

supertestは、Node.jsのHTTPサーバーをテストするためのライブラリです。 型定義ファイルも同時にインストールします。

$ npm i supertest @types/supertest --save-dev

参考: https://github.com/visionmedia/supertest

mochaのインストール

mochaは、テストのためのフレームワークです。 下のコードに出てくる describe とか it とかいう関数はmochaが提供しているものです。 やはり、型定義ファイルも同時にインストールします。

$ npm i mocha @types/mocha --save-dev

参考: https://mochajs.org/

実行スクリプトを追加

package.json内の scripts に以下を追加します。 これにより、 npm run test でテストを実行できるようになります。

"test": "mocha --require ts-node/register test/**/*.ts"

--require ts-node/register により、JavaScriptによってコンパイルすること無くテストを実行できます。

コード

test/routes.ts

とても簡単なものです。

import request from 'supertest';
import 'mocha';

import app from '../src/app';

describe('ルーティング', () => {
  describe('/', () => {
    it('ステータスコード200を返す', async () => {
      await request(app)
        .get('/')
        .expect(200);
    });
  });
});

import mochadescribeit を使うために必要です。

実行

$ npm run test
  ルーティング
    /
      ✓ ステータスコード200を返す
  1 passing (37ms)

テストが通りました。

ビルド・実行する

本番環境で実行する際には、JavaScriptにコンパイル(ビルド)して、その結果生成されたファイルをNodeで実行する必要があります。 このための準備はすでにできていて、以下のスクリプトを実行するだけで大丈夫です。

$ npm run build
$ npm start

AWS S3 の特徴まとめ

AWSを使っている人全員が触ったことがあるであろうS3。整理のためのメモ。

  • データを複数の場所に複製するという特徴から、データの更新・削除には結果整合性が採用されている
  • Getでファイルをダウンロードできる
  • Putでファイルをアップロード(新規/更新)できる(最大5GBまでだが、Multipart Uploadを利用すると5TBまで可能)
  • Listでオブジェクト一覧を取得できる(最大1000件まで)
  • Copyでオブジェクトを複製できる(最大5GBまでだが、Multipart Uploadを利用すると5TBまで可能)
  • Deleteでオブジェクトを削除できる(最大1000個)
  • Headでオブジェクトのメタデータを取得できる
  • RestoreでアーカイブされたオブジェクトをS3に取り出すことができる
  • ユーザーポリシー/バケットポリシー/ACLを用いたアクセス権の決定ができる
  • Get/Putについては、Pre-signed URLを利用することでセキュアに操作を行うことができる
  • 静的なwebサイトをホスティング可能
  • AJAXなどを利用して、異なるドメインからS3でホスティングしているファイルにアクセスしたい場合にはCORSの設定をする
  • ホスティングの際は、CloudFrontとともに使うことを推奨
  • VPC Endpointを利用すると、VPC内のPrivate SubnetからNAT GatewayやNATインスタンスを経由せずに直接S3とセキュアに通信ができる(同一リージョンの場合のみ)
  • クロスリージョンレプリケーションを用いると、異なるリージョン間でオブジェクトのレプリケーションが可能
  • バージョン管理が可能(バケットに対して設定する)
  • ライフサイクル管理を用いると、バケット全体/Prefixに対して、日単位でのストレージクラスの変更/削除処理が可能
  • オブジェクトをアーカイブすると、データはGlacierに移動する
  • S3インベントリを利用すると、オブジェクトのリストをcsvとして取得できる
  • バケットにイベントが発生した際に、SNS/SQS/Lambdaなどに通知(連携)することができる
  • LambdaでS3バケットのイベントを利用したい場合は、Lambdaが利用するIAM RoleにS3の権限を付与する必要がある
  • 大きなサイズのファイルをダウンロードする際にRange Getを利用することで、マルチスレッド環境で高速なダウンロードが可能(Multipart Upload時と同じチャンクサイズを利用)
  • Multipart Uploadはチャンクサイズと並列コネクション数のバランスが重要
  • ファイルが100MBを超える場合に、Multipart Uploadの利用が推奨される
  • Multipart Uploadでは各チャンクは5GB以下である必要がある
  • バケットへのリクエストが定常的に数百RPSを超える場合は、キー名の先頭部分の文字列をランダムにすると良い

参考になるサイト・スライド

aws.amazon.com

www.slideshare.net

AWS DynamoDB についてのメモ

使う機会がったので。

  • SPOF(単一障害点)が存在しない、信頼性の高いマネージド型NoSQLデータベース
  • テーブルごとにRead, Writeそれぞれに対して必要な分のスループットキャパシティをプロビジョンすることができる
  • 運用中にオンラインでキャパシティの設定を変更することが可能(スケールダウンは日に9回まで)
  • Writeは少なくとも2つのAZでの書き込み完了でAck
  • Readはデフォルトで結果整合性のある読み込みとなる
  • Consistent Readオプションを付けることで強力な整合性のあるReadが可能だがCapacity Unitを2倍消費する
  • 1ユニットの書き込みキャパシティユニットによって、最大1KBのデータを1秒間に1回書き込み可能
  • 1ユニットの読み込みキャパシティユニットによって、最大4KBのデータを1秒間に1回読み込み可能(結果整合性のある読み込みで良ければ1秒間に2回)
  • GetItemでPartition Keyを条件として1件のアイテムを取得できる
  • PutItemで1件のアイテムを書き込める
  • Updateで1件のアイテムを更新できる
  • Deleteで1件のアイテムを削除できる
  • QueryでPartition KeyとSort Keyの複合条件にマッチするアイテム群を取得できる
  • BatchGetで複数のプライマリーキーを指定してアイテム群を取得できる
  • Scanでテーブルの全データを取得できる
  • Query, Scanでは最大1MBのデータを取得可能
  • Partition Key + Sort Keyでプライマリーキーとすることができる
  • Sort Keyによって同一のPartition Keyでのデータの並びを保証できる
  • テーブルには任意の数のアイテムを追加できる
  • 1つのアイテムの合計サイズは400KB以下である必要がある
  • Local Secondary Indexについては異なるハッシュキーの値ごとに最大10GBまでのデータを格納できる
  • 4.8KBのアイテムを1秒あたり1000回読み込むためには1000 * 2 = 1000 Read Capacity Unitsが必要
  • スループットはパーティションに均等に付与されているため、アクセスされるキーに偏りが発生しないようにする必要がある
  • パーティション数はストレージ容量とスループットで決まる
  • キャパシティのプロビジョンを増やすとパーティションの数は増えるが、キャパシティをへらすときには各パーティションのキャパシティが減る
  • Burst Capacityによって、パーティションごとに利用されなかったキャパシティを過去300秒分までリザーブできる
  • Local Secondary Indexを用いると、(Partition Key同一のアイテム群の中で)Sort Key以外に絞り込み検索を行うkeyを持つことができる
  • Global Secondary Indexを用いるとPartition Keyをまたいで検索を行うことができる
  • LSI/GSIはスループットやストレージ容量を追加で必要とする
  • セカンダリインデックスに強く依存するような設計になってしまう場合は、RDSの使用を検討する
  • Conditional Writeを用いると、条件付きの書き込み/更新ができる
  • Filter式を用いて、Query/Scanの返却結果を絞り込むことができるが、スループットの節約の効果はない
  • UpdateItemにおいて、PutでAttributeの更新を、AddでAttributeへの足し算/引き算/セットへの追加を、DeleteでAttributeの削除を、それぞれすることができる

参考になるページ・スライド

aws.amazon.com

www.slideshare.net

AWS Key Management Service (KMS) についてのメモ

使う機会があったので。

  • データの暗号化には、通信の暗号化(in transit)と保管データの暗号化(at rest)があるが、KMSは保管データの暗号化に使われる
  • KMSは、鍵の保管・管理を提供する
  • Customer Master Keyは、KMS内部のHSMでのみ平文で存在し、最大4KBのデータを暗号化・復号化できる
  • Customer Master Keyは暗号化された状態で可用性の高いストレージに保管される
  • マスターキーをデータ暗号化に直接利用するのではなく、マスターキーで暗号化した暗号キーでオブジェクトを暗号化/復号化する(Envelope Encryption)
  • 暗号化の基本的な動作は以下の通り
    • 1: クライアントが、CMKのIDとともにkms::GenerateDataKeyを呼び出す
    • 2: KMSが、ユニークなCustomer Data Keyを作成
    • 3: KMSが、暗号化されたCMKをHSM内で復号化
    • 4: KMSが、CMKを用いてCDKを暗号化し、平文のCDKと暗号化されたCDKをクライアントに返す
    • 5: クライアントが、平文のCDKを用いてデータを暗号化し、データとともに暗号化されたCDKを保存する
  • 復号の際は、暗号化されたCDKをKMSに渡す
  • キーポリシーの他に、許可(Grants)を用いてアクセスをコントロールすることができる
  • Grantsを用いると、CMKの利用を他のAWSプリンシパルに委任することができる
  • 暗号化する際にEncryption ContextというKey/ValueペアをKMSにわたすことができ、復号化の際にも同一の値が求められるようになる(CloudTrailのログに平文で出力されるため、機密情報は含めないようにする)
  • Encrypt APIによってデータを暗号化することができ、その結果の暗号文にはヘッダが付与される
  • 元データ、生成された暗号文はAWS内には保持されない
  • 4KBまでの平文データを暗号化できる
  • Decrypt APIによってデータを復号化できる(CMKの指定は不要であり、暗号文のヘッダから該当するCMKが特定される)
  • Generate Data Keyによってデータの暗号化に利用するCDKを生成することができる(平文のデータキーと、Encrypt APIで暗号化されたデータキーを返す)
  • 平文のデータキーは暗号化処理が終了したら即座に削除し、暗号化されたデータキーは暗号化されたデータとともに保存する
  • 復号化の基本的な動作は以下の通り
    • クライアントが暗号化されたデータキーをKMSに送信
    • KMSはマスターキーを利用してデータキーを復号し、クライアントに返す
    • クライアントがデータキーを用いてデータを復号する

参考になるページ

aws.amazon.com

www.slideshare.net

libuvとは(特にイベントループについて)

f:id:k-kty:20190111205332p:plain
libuvのロゴ

http://docs.libuv.org/en/v1.x/design.html を読み進めていった際のメモです。

概要

libuvは「クロスプラットフォームサポートのためのライブラリ」で、Node.jsで使われることを想定して開発が進められてきた(現在はNode.js以外でも用いられている)。

「イベント・ドリブンな非同期I/Oモデル」をもとに設計されていて、以下のようなものを提供している

  • プラットフォーム間で異なるI/Oポーリングの仕組みの抽象化
  • (ハンドルとストリームによる)ソケットなどの抽象化
  • ファイルI/Oやスレッド周辺の各種機能

ハンドルとリクエスト

libuvでは、イベントループとともに、ハンドルとリクエストという2つの抽象化された概念が用意されている。

ハンドルは「(アクティブなときに)何らかの操作を行うことができる、長期的に存在するオブジェクト」を表す。その例としては以下のようなものがある。

  • イベントループが一周するたびに一度呼ばれるprepareハンドル
  • 新しいコネクションができるたびにコールバックを実行するTCPサーバーハンドル

リクエストは(一般的に)「短い期間で終わる有効な操作」を表す。 リクエストはハンドルを通して行う(例:writeリクエストはハンドルを通してデータを書き込む)ことも、スタンドアロンで行う(例:getaddrinfoリクエストはハンドルを用いず、ループの中で直接実行される)こともできる。

I/Oループ (イベントループ)

I/Oループ(イベントループ)は、libuvの中核をなしている。 イベントループはすべてのI/O操作を管理し、「一つのイベントループが一つのスレッドに対応する」ことが想定されている。

異なるスレッドで動作する限り、複数のイベントループを利用することができる。 ちなみに、イベントループ(そして、ループやハンドルに関係する他のAPI)は、基本的にはスレッドセーフではない。

イベントループは、I/Oに対してシングルスレッドの同期的な(一般的な)アプローチを用いている。 すべての(ネットワーク)I/Oはノンブロッキングなソケット上で行われ、それをプラットフォームに応じた最善の方法でポールしている(Linux上ではepoll, OSXやその他のBSDではkqueue, SunOSではevents port, WindowsではIOCP)。ループの中において、イベントループは(それ以前にポーラーに追加された)ソケット上でのI/Oアクティビティを待ち、ソケットの状況(読み込み可能, 書き込み可能, ハングアップ)とともにコールバックが呼ばれる、これを用いてハンドルはI/O操作を行える。

イベントループへの理解を深めるために、一回のループでどのようなことが行われているのかを確認する。

  1. 「現在時刻」が更新される。イベントループは、ループの最初に取得した時刻をキャッシュすることで、時刻関係のシステムコールの発行回数を減らしている。
  2. もしもループが生きているのなら、イベントループの周回がはじまる。もしも生きていないのなら、イベントループは終了する。(有効かつ参照されているハンドル、有効なリクエスト、またはclosingハンドルが存在する場合、イベントループは生きているとみなされる。)
  3. 適切なタイマーが実行される。イベントループの「現在時刻」よりも前に設定されていたコールバックが呼ばれる。
  4. pendingコールバックが呼ばれる。多くのI/Oコールバックは、I/Oのポーリングの直後に呼ばれることが多い。しかし、コールバックの実行を(イベントループの)次の周回に遅らせるということもある。そのようにして前の周回で後回しにされたI/Oコールバックが、ここで実行される。
  5. idleコールバックが呼ばれる。
  6. prepareハンドルのコールバックが呼ばれる。これはイベントループがI/Oブロックをする直前に呼ばれる。
  7. pollタイムアウト(どれほどの時間I/Oのブロックを行うか)を計算する。計算は以下の通り行われる。
  8. 一つ前のステップで計算した時間分だけ、I/Oのブロックをする。(読み書きのために)ファイルディスクリプタを監視しているI/O関連のハンドルはここで適当なコールバックを呼ぶ。
  9. checkハンドルコールバックが呼ばれる。checkハンドルのコールバックはI/Oのブロックの後に呼ばれ、prepareハンドルのコールバックと対を成している。
  10. closeコールバックが呼ばれる。もしもハンドルが uv_close() によってcloseされた場合、そのハンドルはcloseコールバックを呼び出す。
  11. UV_RUN_NOWAIT または UV_RUN_ONCE モードでループが走っている場合には、ループは終了する (uv_run() がreturnする)。もしも UV_RUN_DEFAULT モードでループが走っている場合には、ループが生きてれば次の周回に入り、そうでなければループは終了する。

注1: 7の計算は以下のように行われる

  • UV_RUN_NOWAIT フラッグとともにループが走っていた場合、0
  • uv_stop() が呼ばれている場合(イベントループがもうすぐ止まる場合)には0
  • アクティブなハンドルやリクエストが存在しない場合には0
  • もしもアクティブなidleハンドルが存在する場合には0
  • 「closeされるべきハンドル」が存在する場合には0
  • 上の場合全てにマッチしない場合には最も直近のタイマーの時間(もしもそれが存在しない場合には無限)

注2: libuvはファイルI/Oにおいてスレッドプールを用いているが、ネットワークI/Oは常にシングルスレッド(イベントループのスレッド)上で行う。

注3: pollingの機構はUnixとWindowsで大きく違うが、libuvは実行モデルに一貫性をもたせている。

ファイルI/O

ネットワークI/Oとは違い、(設計上の困難さから)ファイルI/Oはスレッドプールを用いてファイルのI/Oを行っている。

libuvは現在グローバルな(全てのイベントループが用いることのできる)スレッドプールを用意している。3種類の操作がこのプールで行われる。

  • ファイルシステムの操作
  • DNS関連の操作(getaddrinfo と getnameinfo)
  • ユーザーが定義した操作 (uv_queue_work() を利用する)

注: スレッドプールのサイズは限られているので、注意を配る必要がある

V8 JavaScriptエンジンとは(そしてその高速化について少し)

f:id:k-kty:20190111205205p:plain
V8のロゴ

V8とは

V8はGoogleが開発しているオープンソースのJavaScript実行エンジン(JavaScriptのコードを解析し、機械語まで落とし込んで実行してくれるもの)です。

C++で書かれていて、Google Chromeで使われているほか、Node.js(サーバーサイド向けのJavaScript実行環境)などにも組み込まれています。

競合?として、MicrosoftのChakraやMozillaのSpiderMonkeyなどがありますが、全体的にパフォーマンスはV8に分があります。

公式サイトは https://v8.dev/ です。

JavaScriptエンジンのパフォーマンスについて

JavaScriptの実行エンジンで最も重要なのは「いかに高速にJavaScriptを実行できるか」です。

「どれだけ広いJavaScriptの仕様に対応できるか」や「デバッグ機能の豊富さ」も忘れてはならない点ではあるものの、やはり一番差が出るのはパフォーマンスで、そして実行エンジンのパフォーマンスの差はブラウザのパフォーマンスに大きく影響します。

そのために、ブラウザ(熾烈なシェア争いを繰り広げている)の開発元は実行エンジンのパフォーマンスの向上に努めてきました。

そのおかげか、JavaScriptは動的型付けの言語にもかかわらず、実行エンジンのおかげでかなりのハイパフォーマンスで実行することができるようになっています。(似たようなCPUインテンシブな処理をPythonとJavaSriptで書いてみて比較したらJavaScriptのほうが早いことが多い。)

V8の高速化について

そこで実際、V8がどのようにしてJavaScriptの実行を高速化をしているのか(一例だけど)を軽くまとめてみます。

  1. JavaScriptのコードがパースされ、抽象構文木が作られる。
  2. Ignitionというインタープリタが、その抽象構文木を元に、実行を始める(最適化するよりも、とにかく早く実行を開始するという雰囲気)
  3. Ignitionでの実行中に、ホットな関数(多く呼び出される関数)を特定し、さらにその関数がどういった種類(文字列なのか、数値なのか)の引数とともに呼び出されているのか、というデータを収集する
  4. そのデータをもとに、TurboFanがホットな関数をコンパイルし、最適な機械語を生成する
  5. その機械語を用いた実行を始める
  6. もしも異なったタイプの引数が来てしまった場合(最適化した機械語を用いられない場合)は最適化前の処理に戻る

こういった感じでV8はJavaScriptを実行するらしいです。

特定の関数の引数には常に同じ型のものを渡してあげると、実行エンジンに優しいコードになるということですね。