スパイダープラス Tech Blog

建設SaaS「スパイダープラス」のエンジニアとデザイナーのブログ

TypeScriptとの比較で解き明かすRustのメモリ管理

皆様こんにちは。現在スパイダープラスはフロントエンド・バックエンドにTypescriptを用いて、型安全なWebアプリケーション開発を行っています。

TypeScriptを用いることで、型の安全性を確保しつつ、高い生産性で開発を進められることが実感できています。

一方で、安全性やパフォーマンスを重視するシステム開発にRustを採用する試みを進めています。

Rustは、TypeScriptとは異なるアプローチでメモリ管理と安全性を実現しています。

この記事では、TypeScript/JavaScriptを基準にしながら、Rustが保証する「メモリ安全性」とは何なのかを探ります。メモリ管理の仕組みなど「安全性」に対する考え方の違いを解説します。


TypeScriptのメモリ管理の仕組み

まず、TypeScript/JavaScriptでコーディングする時どのような仕組みとなっているかを解説します。

TypeScript/JavaScriptでコーディングするとき、ガベージコレクタ(GC)が定期的にゴミをメモリから掃除してくれるので、「このオブジェクトが使われなくなったら、メモリを解放しよう…」と考えずに開発を進められます。

では、GCは具体的にどうやって「ゴミ」を見つけ出すのでしょうか?

これはマーク&スイープ(Mark and Sweep)というアルゴリズムによって実現されています。

マーク&スイープとは簡単に説明すると、まずプログラムのルート(グローバル変数や関数の引数など)から辿れるオブジェクトを「マーク」し、次にマークされなかったオブジェクトを「スイープ(解放)」するという仕組みです。以下、ステップで解説します。

ステップ1: マークフェーズ

  • 下図で説明します。プログラムの「ルート」から開始し、参照をたどります。下図では globalがルートに相当し、グローバル変数、現在実行中の関数のローカル変数、関数の引数など、確実にアクセス可能な場所を表します。
  • ルートから辿れるすべてのオブジェクト(図中の node1node9)を再帰的に探索し、「到達可能なもの」にマークをつけます。これらのノード間には相互参照や循環参照があっても、ルートから辿れる限りすべてマークされます。

ステップ2: スイープフェーズ

  • 次に、メモリ上のすべてのオブジェクトをスキャンし、マークがついていないオブジェクト(=どこからも参照されていないゴミ)を見つけます。図中では「削除対象 (unreachables)」のサブグラフ内にある unr1unr2unr3がこれに該当します。これらのノードは互いに参照し合っていても、globalから到達できないため削除対象となります。
  • それらのメモリ領域を解放し、再利用可能にするという仕組みとなります。

マーク・アンド・スイープ

この仕組みによって、開発者が明示的に deletefreeのような命令を書かなくても、使われなくなったメモリは自動的に掃除されます。

これにより、開発者はメモリ管理を意識することなく開発を進めることができ、アプリケーションのビジネスロジックに集中することができます。これはアプリケーション開発においては大きなメリットとなり、高い生産性をもたらすことが期待されます。


Rust - メモリ領域「スタック」「ヒープ」「静的領域」

TypeScriptと違い、RustはGCを持ちません。その代わり、プログラムが利用する主要なメモリ領域、「スタック」、「ヒープ」、「静的領域」を意識して開発を行う必要があります。

なぜメモリ領域を使い分ける必要があるのか?

なぜわざわざメモリ領域を「スタック」「ヒープ」「静的領域」と分けて使う必要があるのでしょうか?

それは、Rustがコンパイル時にメモリ配置を確定させる必要があるからです。

TypeScript/JavaScriptではGCが実行時に動的にメモリを管理してくれますが、Rustには実行時のGCがありません。その代わり、コンパイル時に「どのデータをどこに配置し、いつ解放するか」を決定します。これにより、実行時のオーバーヘッドなしで安全なメモリ管理を実現しているのです。

そのためRustでは、すべての型のサイズがコンパイル時に確定している必要がありますコンパイラが「この変数にはN バイト必要だ」と事前に知ることができれば、スタックに効率的に配置できます。逆に、実行時にしかサイズが分からないデータ(可変長の文字列や配列など)は、柔軟に伸縮できるヒープ領域に配置する必要があるのです。

一見面倒に思えるこの制約がRustの安全性や高速性を実現するための重要な要素となります。以下、それぞれのメモリ領域について詳しく見ていきましょう。

スタック (Stack)

スタックは高速にアクセスできるメモリ領域です。関数が呼び出されるとデータが積まれ(プッシュ)、関数が終了すると自動的にデータが降ろされます(ポップ)。i32のような数値型、bool、固定長の配列など、コンパイル時にサイズが確定しているデータが置かれます。

ヒープ (Heap)

ヒープはスタックと比べると低速ですが、柔軟に利用できるメモリ領域です。プログラムの実行中、任意のタイミングでメモリ領域を確保し、不要になったら解放します。StringVec<T>(可変長配列)など、実行時にサイズが変わる可能性のあるデータが置かれます。

静的領域 (Static Storage)

静的領域はプログラムの実行期間中ずっと存在するメモリ領域です。文字列リテラル(例: "Hello, world!")やグローバル変数など、プログラム全体で共有されるデータが置かれます。static変数もここに配置されます。

TypeScript/JavaScriptではGCがこれらのメモリ管理を自動的に行いますが、Rustでは開発者がこれらを意識して使い分ける必要があるのです。


実例で見るメモリ配置

このスタックとヒープの使い分けが、TypeScriptとRustでどう異なるのか、具体的な例で見ていきましょう。

TypeScriptの場合

まず、TypeScriptでコードを書く際のメモリ管理を考えてみましょう。

// 文字列
const message = "Hello, world!";
let greeting = "Hello";
greeting += ", world!"; // 文字列の連結

// 配列
const numbers = [1, 2, 3];
numbers.push(4); // 要素の追加

TypeScript/JavaScriptでは、このコードを書くとき、文字列がどのメモリ領域に保存されるかを意識することはありません。JavaScriptエンジンが自動的に最適な場所にメモリを割り振ってくれます。

Rustの場合 - 明示的なメモリ管理による安全性

一方、Rustの場合を確認してみましょう。Rustでは、文字列と配列に対して2つの異なる型が用意されており、それぞれが異なるメモリ領域に保存されます。

1. 文字列: &strString

TypeScriptでは文字列は string型で扱いますが、Rustには主に2種類の文字列型があります。

&str (文字列スライス / 文字列リテラル):

  let s1: &str = "Hello, world!";

&strは参照型(データそのものではなく、「データのありかを示す情報(アドレス)」を持つ型のこと)なので、「スタック」上に変数 s1が格納され、s1が「文字列データへのポインタ」と「長さ」という2つの情報を持ちます。実際の文字列データ("Hello, world!")自体は「静的領域」に保存されます。s1はそれを指し示しているだけです。代入時には、このポインタと長さの情報がコピーされます(参照のコピー)。本体が読み取り専用なので、&strの中身を後から変更することはできません。

String (文字列型)

一方で、String型は後から文字を追加したり変更したりできる動的な文字列です。

  let mut s2: String = String::from("Hello");

この文字列の実際のデータ ("Hello") は「ヒープ領域」に確保されます。そして s2という変数は、そのヒープデータへのポインタや現在の長さ、確保済みの容量といった管理情報を「スタック」上に保持します。ヒープにあるからこそ、s2.push_str(", world!");のように後からデータを追加できるのです。

&strとの違いは、実際の文字列データが「静的領域」ではなく「ヒープ領域」に配置される点です。ヒープは動的に確保・解放できるため、文字列の長さを後から変更できます。

2. 配列: [T; N] vs Vec<T>

配列も同様です。

  • 配列 [i32; 3]:
  let mut a: [i32; 3] = [1, 2, 3];
  a[0] = 10; // OK! 配列の個々の要素は変更できる
  // a.push(4); // コンパイルエラー! 要素の追加はできない

[i32; 3]は「i32型の要素が3つ」という固定長の配列です。サイズがコンパイル時に確定しているため、このデータは丸ごとスタック上に確保されます。

重要なポイント:

  • スタックに保存されているため、配列のサイズ変更(要素の追加・削除)はできません
  • しかし、配列の各要素の値は変更できます(mutが必要)
  • これは、スタック上の固定サイズ領域内で値を書き換えるだけなので、メモリレイアウトに影響しないためです
  • ベクタ Vec<i32>:
  let mut v: Vec<i32> = vec![1, 2, 3];
  v[0] = 10;  // OK! 要素の値を変更
  v.push(4);  // OK! 要素の追加
  v.pop();    // OK! 要素の削除

Vec<T>は可変長の配列で、TypeScriptの Arrayに近いです。実際のデータはヒープ領域に確保され、vStringと同様に管理情報をスタックに持ちます。

重要なポイント:

  • ヒープに保存されているため、配列のサイズ変更(要素の追加・削除)が可能
  • もちろん、各要素の値も変更できます
  • ヒープ領域は動的に伸縮できるため、容量が足りなくなれば、より大きなメモリ領域を確保し直してデータを再配置します

ここまで見てきたように、TypeScriptでは string 1つ、Array<T> 1つで済むのに対し、Rustでは &strString[T; N]Vec<T> という2つの型を使い分ける必要があります。


なぜRustはスタックとヒープを使い分けるのか?

ここで自然な疑問が浮かびます。「そもそも、全部ヒープに保存すれば、型を2つに分ける必要はないのでは?」

実際、TypeScript/JavaScriptはまさにそのアプローチを取っており、GCのある言語では基本的にほとんどのデータをヒープに置いて管理しています。しかし、Rustがスタックとヒープを使い分けるのには、明確な理由があります。

1. パフォーマンス - スタックは高速

スタックとヒープでは、メモリの確保・解放にかかる時間に大きな違いがあります。

スタックは高速にアクセスできるメモリ領域です。メモリの確保と解放はポインタの移動だけで完了するため、数ナノ秒で済みます。

一方、ヒープはメモリアロケータと呼ばれるヒープメモリを管理するシステムが、メモリの中から適切な空き領域を探索し、確保するのに時間がかかります。

ヒープ領域の容量は、ヒープ上に「とりあえずこれだけ確保しておいた」メモリ全体のサイズです。上の String型の図では容量を5バイト確保しています。例えば、pushなどでそれに対して文字を追加すると確保した容量5バイトでは足りなくなるケースがあります。

この場合、もっと大きなメモリをヒープに確保する必要があり、この作業を再確保(リアロケーション)と呼びます。リアロケーションは新しいメモリ領域を確保し、既存のデータをコピーし、古いメモリを解放するという一連の操作が必要になるため、非常にコストが高くなり、その分動作が遅くなるのです。

2. メモリ効率 - 管理コストが無駄

ヒープにデータを配置する場合、実際のデータサイズに加えて、メモリ管理のためのオーバーヘッドが必要になります。

例えば、4バイトの整数をヒープに配置すると、データ本体の4バイトに加えて、メモリアロケータが管理情報として16〜32バイトのオーバーヘッドを必要とします。

一方、スタックに配置する場合、このような管理オーバーヘッドは一切不要です。4バイトの整数は、そのまま4バイトのメモリ領域だけで済み、メモリ効率は100%です。

全てをヒープに配置すると、本来必要のない管理コストで大量のメモリを浪費することになります。

3. コンパイル時最適化 - コンパイラが賢く最適化できる

スタック上のデータは、コンパイラが最適化を行います。

例えば、コンパイルする時に常に同じ結果になることがわかれば、その計算を定数に置き換えることができます。 以下のような、スタック上の変数だけを使う単純な関数を考えます。

fn add_on_stack() -> i32 {
    let a: i32 = 2;
    let b: i32 = 3;
    a + b
}

この関数は、常に 5 を返すことがコンパイル時にわかるため、コンパイラはこの関数を単に 5 を返すコードに置き換えることができます。これにより、わざわざ実行時に関数を呼び出して、スタックに abのメモリを確保して計算する必要がなくなります。

一方、ヒープへのメモリ確保は「副作用」と見なされるため、コンパイラは簡単に省略できません。たとえ計算結果が常に同じであっても、メモリ確保の操作は保持される必要があります。このため、ヒープを使う場合は最適化の余地が制限され、実行時のパフォーマンスを阻害する可能性があるのです。

スタックを活用することで、コンパイラはより積極的な最適化を行い、実行時のコードサイズと実行速度の両方を改善できるのです。

4. データサイズによる使い分け - 巨大なデータはヒープが有利

ここまでスタックの利点を説明してきましたが、常にスタックが最適というわけではありません。

データサイズが大きい場合、スタックに配置するとコピーコストが問題になります。例えば、数メガバイトの画像データや大きな構造体を関数間で受け渡す場合、スタック上でそのまま扱うと、関数呼び出しのたびにデータ全体がコピーされてしまいまい、パフォーマンスが低下する可能性があります。

// 巨大な構造体の例
struct LargeData {
    buffer: [u8; 1_000_000], // 1MBの配列
}

fn process_data(data: LargeData) {
    // data全体(1MB)がコピーされる
}

一方、ヒープにデータを置き、スタックにはそのポインタだけを持つようにすれば、関数呼び出し時にコピーされるのはポインタだけで済みます。

fn process_data(data: Box<LargeData>) {
    // ポインタ(8バイト)だけがコピーされる
}

このように、Rustでは小さなデータは高速なスタックに、動的なサイズや巨大なデータはヒープにという使い分けが重要になります。


まとめ

ここまで見てきたように、TypeScriptとRustのメモリ管理には大きな違いがあります。

TypeScriptの特徴:

  • ガベージコレクタが自動的にメモリの配置と解放を行う
  • 開発者はメモリを意識せずにコーディングでき、アプリケーションロジックに集中できる
  • しかし、GCの動作タイミングは予測不可能で、パフォーマンスへの影響や意図しないメモリリークの可能性がある
  • メモリの仕組みを理解していないと、クロージャによる意図しない参照保持や、大きなオブジェクトの不要なコピーなど、気づかないうちにパフォーマンス上の問題を引き起こす可能性がある

Rustの特徴:

  • 型システムを通じて、データをどこに配置するかを明示的に制御する
  • &strは静的領域、Stringはヒープ、[T; N]はスタック、Vec<T>はヒープ、というように配置場所を明確に意識して使い分ける
  • コンパイル時にメモリ配置が確定し、メモリ解放のタイミングも確定的

TypeScriptはメモリを意識せず開発ができる反面、不要になった値がいつ解放されるか分からず、メモリリークを引き起こす可能性は否定できません。

一方でRustはメモリ管理を明示的に行う必要がありますが、その分パフォーマンスや安全性を高いレベルで保証できます。

どちらが優れているという話ではありませんが、Rustの仕組みを学ぶことで、TypeScriptでは意識しなかったメモリや、型システムの本質に気づくことができます。

このブログを通して、Rustのメモリ管理の仕組みを理解することで、普段は意識していなかったメモリの扱い方や、GCに任せることのトレードオフに気づき、さらなる技術的洞察を得られることを願っています。

皆様もこの機会に、ぜひRustの世界にも触れてみてはいかがでしょうか?

スパイダープラスでは、エンジニアを募集しています。興味のある方はぜひ、カジュアル面談からでもお気軽にご応募ください!

TypeScriptだけでなくRustの経験者の方も大歓迎です。

なぜ今、基礎資格?QA歴10年弱の私が、あえてJSTQB FLに挑戦する理由

こんにちは。プロダクト品質部の後藤です。

最近、「JSTQB認定テスト技術者資格 Foundation Level」の取得に向けて学習を始めました。

今回は、私がなぜこの資格を受験しようと思ったのか、現在どのような風に学習を進めているかについて書こうと思います。ちなみに、試験はまだ受けていません。

JSTQB認定テスト技術者資格 Foundation Levelってこんな資格

JSTQB認定テスト技術者資格」は、ソフトウェアテストに関する知識とスキルを認定する、日本国内における実質的な標準資格です。

続きを読む

「ユーザーからの"なぜか●●"を紐解く!CREの不具合調査日誌」

はじめまして。 スパイダープラス CREの森川です。

24歳でIT業界に入り、出産を経てからは、システム保守やテクニカルサポートといった分野で経験を積んできました。そんな私の最近一番の難題は、仕事のトラブルシュートより難しい、思春期の子どもとのコミュニケーションです。

さて、私の所属するCREチームは、お客様が直面する技術的な課題を解決し、サービスの安定稼働を支えるミッションを元に日々活動しています。メンバーがそれぞれ異なる強みを持ってるため、一人では解決が困難な問題も、チームとして連携することで乗り越える場面が多々あります。このような『チームへ貢献する意識』がその先にいるお客様の課題を解決したいという、自然なモチベーションの源泉になっていると感じています。

続きを読む

「アジャイル、始めました」がゴールになってない?形骸化しないスクラムのための、はじめの一歩

こんにちは。スパイダープラスで開発チームのEMを担当している細矢です。

近年、ソフトウェア開発においてアジャイル開発の考え方が浸透し、「うちのチームもアジャイルです!」という言葉を耳にする機会が増えました。

とはいえ、「これから導入を検討している」というチームや、「始めてはみたものの、本当に上手くいっているのだろうか?」と悩んでいるチームも、まだまだ多いのではないでしょうか。

この記事では、そういった方々に向けて、弊社での最近の取り組みをご紹介します。アジャイル開発をこれから始める方、そして既に始めているけれどもしっくりきていないと感じている方にとって、「はじめの一歩」を踏み出すヒントになれば幸いです。

この記事はこんな人におすすめです

  • これからアジャイル開発やスクラムを始めようとしている方
  • スクラムを導入してはみたものの、なんだか形骸化していると感じている方
  • チームの目的意識を再確認し、パフォーマンスを向上させたいと考えているマネージャーやリーダーの方
続きを読む

野菜作りをするQAエンジニアが考えていること

今回は様々なバックグラウンドを持つ社員が在籍する中で、QAエンジニアとして活躍しつつ本格的な野菜作りもしている、異例な存在である岡野に話を聞いてきました!

インタビュー:岡野さんのQA業務と野菜作り

「まずは簡単に経歴についてお聞かせいただけますか?」

岡野: ゲーム関連業界で長年品質管理に携わってきました。最初は遊技機のデバッグからスタートし、その後ソーシャルゲームの開発現場でQAエンジニアとして活躍しています。バグの発見と修正にやりがいを感じていて、製品を完璧に近づけることにモチベーションを持っています。パチスロの押し順に関する好奇心が、業界入門のきっかけになったんです。

「現在のスパイダープラスでの業務を教えてください」

岡野: スパイダープラスでのQA業務に集中しています。仕様の理解が難しい場面もありますが、マニュアルと実際の動作を照らし合わせながら、体で覚えるように克服しています。テスト作業中には集中できる「ゾーン」に入り、時間の経過を忘れるほど没頭します。業務における課題として、仕様の理解を挙げ、根気強く対応しています。私の仕事観は、製品の品質を高めることで社会に貢献するというものです。

続きを読む

MCP Serverを使ってECSを自然言語で操作してみた 〜AWSインフラ運用を"対話"で完結させるアプローチ〜

こんにちは。
PF開発部 / SREチーム所属の平木です。

💡はじめに

今年に入って公開されたAmazon ECS MCP Serverを試してみたので、その内容をまとめました。

 

普段ECSを運用していると、コンテナにログインして hostname を確認したり、
タスクを再起動するなど、ちょっとした操作を行うことがあります。
「これ、自然言語でできたら便利じゃない?」と思ったことはありませんか?

実際にMCP Serverを導入して、チャット画面からECS操作を行える環境 を構築した結果を紹介します。

続きを読む

AI AgentとMCP Serverで実現するiOSアプリの自動テスト作成の効率化

こんにちは、スパイダープラスでiOSアプリエンジニアをしているピヨコです🐣 

2020年入社。2025年8月まではEM、9月からエンジニアに復帰しました。スノーボードと野球観戦が好きです。

本記事はテックイベント「【実践事例4選】AI Agentで変わるモバイルアプリのテスト」(2025/9/29)登壇内容のダイジェストです。

trident-qa.connpass.com

この記事で取り扱うこと

  • 既存のiOSプロダクト(手動検証で一定の安定性がある想定)におけるテスト自動化を、AI Agentを活用して加速する方法

この記事で取り扱わないこと

  • 新規プロダクトの自動テスト導入論
  • 良いプロンプトの一般論
  • 初期の環境構築全般
続きを読む