新しいPython風プログラミング言語Mojoを試してみた

2023.10.24

こんにちは。CX事業本部Delivery部のakkyです。

少々旧聞となりますが、今年9月にMojo言語がローカルで実行できるようにリリースされました。

MojoはSwiftの開発者が立ち上げたModular社が開発している新しいプログラミング言語で、Pythonの文法とRustのメモリ安全性を兼ね備えたコンパイラ型プログラミング言語です。

AI開発に使用することが想定されていて、SIMDのファーストクラスサポートなども特徴的です。実際にllama2.mojoというLlama2の実行環境の実装も行われています。

現在はPythonとの完全な互換性はありませんが、Pythonインタプリタを呼び出すことでPythonコード/ライブラリを呼び出すことができ、将来的にはMojo自体がPythonのスーパーセットとなることを目指しているそうです。

10月19日にはMacのApple silicon(ARM)版もリリースされましたので、M1/M2マシンでも試せるようになりました。

Pythonとの関係

文法

関数の定義にdeffnが存在するのが大きな違いです。defはPython互換で型を付ける必要がなく、fnはMojo独自の関数で型定義などをする必要がありますが、大幅に最適化されます。 defで定義された関数もfnで定義された関数も、それぞれ相互に呼び出して使うことができます。

なお、型を付ける必要があるという違いのほか、fnでは例外をきちんと処理しなければならないという違いもあります。詳しくは公式ドキュメントで説明されています。

また、fnでは変数宣言が必要で、let(イミュータブル・変更不可能)とvar(ミュータブル・変更可能)を明示できます。型推論がありますが、明示的に書くこともできます。

ライブラリ

ライブラリの一覧はドキュメントに記載されています。 現在のところ、Python標準ライブラリがすべて実装されているわけではなく、一部の実装に限られています。実装されているものでも引数や使い方が異なっていることがあるので、ライブラリに関しては完全に別物と思っていたほうがいいと思います。

互換性

MojoではPythonのライブラリをインポートして使うことができます。たとえば、次のようなコードを書くと、requestsでHTTPアクセスできます。

from python import Python

def main():
    let requests = Python.import_module("requests")
    let url = "https://checkip.amazonaws.com/"
    let responce = requests.get(url)
    let ip_raw = responce.content.decode()
    let ip_str = ip_raw.strip()
    let message = "your ip address is " + ip_str.to_string()
    print(message)

ただし、この場合requestsライブラリやその後の処理はMojoではなくCPythonのインタプリタが実行するため、高速化はされないようです。

このコードで言うとrequestsオブジェクトはもちろんのこと、ip_rawなどのオブジェクトもPythonのものになります。Mojoの文字列にはstrip()メソッドはないのにこのコードが動くのは、ip_rawはPythonのオブジェクトで、実際の処理はCPythonインタプリタが行っているためです。

REPLで実行すると変数の型が表示されて、urlはmojoネイティブのStringLiteral型ですが、ip_rawPythonObject型になっていることがわかります。

インストール

公式サイトの手順通りに行います。Ubuntu 23.10では、事前にpython3-venvをインストールしておく必要がありました。venvなしでインストールするとエラーになるのですが、この状態では再びmodular install mojoしてもダメで、modular cleanしてからやり直す必要がありました。

実行方法

mojoコマンドでREPLが起動します。ファイルに保存されているスクリプトを実行するときはmojo run ファイル名とします。 なお、ファイル名は.mojoまたは.🔥という拡張子(絵文字も可!)である必要があります。

mojo build ファイル名とすると、コンパイルして実行ファイルを生成することも可能です。

実行速度を実験してみた

Mojoは高速化が大きな目玉とされていますが、実際はどの程度高速化されるのでしょうか?既存のソフトウェアがそのまま動く状態ではないため、簡単なコードでの実験となりますが、比較してみました。比較対象はCPythonとPypyです。

実行速度の測定にはtimeコマンドを使用し、3回実行して最も速いrealの値を記載しました。 実行環境はWSL2上のUbuntu 23.10です。

使用バージョン

  • Mojo 0.4.0
  • Python 3.11.6
  • PyPy 7.3.12 (Python 3.9.17)

フィボナッチ数の計算

まずはフィボナッチ数の計算で比較してみます。

Python/Pypy

import sys

def fibonacci(n):
    if n == 0 or n == 1:
        return n
    else:
        return fibonacci(n - 2) + fibonacci(n - 1)

def main():
    print(fibonacci(int(sys.argv[1])))

main()

Mojo

2バージョンで比較してみます。まずはdefで定義したものです。こちらは型を付けていません。

Pythonコードとの違いは、sys.argv()がプロパティではなく関数である点と、文字列を数値に変換するのにatol()を使うという点です。

import sys

def fibonacci(n):
    if n == 0 or n == 1:
        return n
    else:
        return fibonacci(n - 2) + fibonacci(n - 1)

def main():
    print(fibonacci(atol(sys.argv()[1])))

次にfnで定義したものです。Int型を指定しました。mainもfnとしたので、例外をキャッチして握りつぶすコードを追加しています。

import sys

fn fibonacci(n: Int) -> Int:
    if n == 0 or n == 1:
        return n
    else:
        return fibonacci(n - 2) + fibonacci(n - 1)

fn main():
    try:
        print(fibonacci(atol(sys.argv()[1])))
    except:
        pass

実行速度の比較

引数を40とした場合。

実行環境 実行時間(秒)
Mojo(def) 8.634
Mojo(fn) 0.430
Python 17.262
Pypy 7.442

Mojoでもdefを使うとあまり高速化しませんが、fnを使うと別物のように高速化しました。

竹内関数の計算

もう一つ、フィボナッチ数の計算と傾向の違いはあまりないと予想できますが、プログラミング言語の処理系のベンチマークに使われる竹内関数も実験してみました。

Python/Pypy

import sys

call_cnt = 0

def tarai(x, y, z):
    global call_cnt
    call_cnt += 1
    if x <= y:
        return y
    else:
        return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y))

def main():
    arg = sys.argv
    if len(arg) >= 4:
        x = int(arg[1])
        y = int(arg[2])
        z = int(arg[3])

        res = tarai(x, y ,z)
        print(call_cnt)

main()

Mojo

こちらはmainをdefで定義してみました。(実際の処理はtarai関数が行うので、実行速度にほとんど変化はないはずです)

import sys

var call_cnt = 0

fn tarai(x: Int, y: Int, z: Int) -> Int:
    call_cnt += 1
    if x <= y:
        return y
    else:
        return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y))

def main():
    let arg = sys.argv()
    if len(arg) >= 4:
        let x = atol(arg[1])
        let y = atol(arg[2])
        let z = atol(arg[3])

        let res = tarai(x, y ,z)
        print(call_cnt)

実行速度の比較

引数を13 6 0とした場合。

実行環境 実行時間(秒)
Mojo(fn) 0.213
Python 4.940
Pypy 1.528

考察とまとめ

フィボナッチ数や竹内関数のような主に関数の呼び出しがメイン負荷となるベンチマークでは、おおむね20倍程度高速化する傾向があることがわかりました。

実際のワークロードではここまでの差は出ないかもしれませんが、SIMDのネイティブサポートなどによってCPUバウンドな処理ではかなり高速化されそうです。

たとえば、Modular公式ブログではPythonから68,000倍高速化したという記事がありますが、これはMojo自体の速度に加えて、SIMDを活用した高速化とマルチコアCPUを活用した最適化の結果のようです。簡単に並列処理ができるのはとても良いですね。

ただ、現在のところは互換性や機能の不足によって、PythonやPypyを置き換えられるような状態にはなっていません。 今後開発が進み、CPythonとの互換性が高まると、既存のPythonプログラムが手軽に高速化できるようになり、他の言語で開発された拡張モジュールを使う必要がなくなるかもしれません。

AWSへの適用という点を考えると、実行ファイルをコンパイル可能なので、Lambdaの実行が高速になるのに加え、デプロイが簡単になるというメリットがありそうです。Webフレームワークが出てくると楽しそうですね。

今後も引き続き開発の状況を追いたいと思います。

参考ページ

InfoQ Mojoプログラミング言語の紹介