【PHP】データ構造内の情報の重複を避けつつ処理速度も高速にする方法

  • 2022年3月25日
  • PHP

 よく何がしかのまとまったデータをクラスなり何なりで構造化してまとめます。これは PHP のクラスであれば次の様にできます。

<?php

class TimeRange
{
    public function __construct(
        public int $startSec, // 開始時刻(秒)
        public int $endSec, // 終了時刻(秒)
        public int $duration // 長さ
    ) {
    }
}

// 1秒目から5秒目までの長さ4秒の範囲
$range = new TimeRange(1, 5, 4);
// 1秒目から12秒目までの長さ11秒の範囲に変える
$range->endSec = 12;
$range->duration = $range->endSec - $range->startSec; // 各プロパティの値を変えて変化を表す

 データ構造中の重複を避けるというのは、他の値によって一意に定まる値を値で持つのをやめ、定め方によって持つということです。これを行うことでデータを無用に定義したり、不正な状態を作ったりすることを減らせます。例では開始と終了と長さのうち二つが定まれば残り一つも自動的に定まります。ここでは長さを開始と終了から定める様にコードを変更します。この手法は様々なところで紹介され、最近は達人プログラマーでも見ました。

達人プログラマー(第2版): 熟達に向けたあなたの旅 | Thomas,David, Hunt,Andrew, 雅章, 村上 |本 | 通販 | Amazon

 実装例は次です。

<?php

class TimeRange
{
    public int $startSec; // 開始時刻(秒)
    public int $endSec; // 終了時刻(秒)

    public function __construct(int $startSec, int $endSec)
    {
        $this->startSec = $startSec;
        $this->endSec   = $endSec;
    }

    /** 長さ */
    public function duration(): int
    {
        // 長さは始めと終わりから導けます
        return $this->endSec - $this->startSec;
    }
}

// 1秒目から5秒目までの長さ4秒の範囲
$range = new TimeRange(1, 5);
echo $range->duration();// 4 // 長さを得られます。

// 1秒目から12秒目までの長さ11秒の範囲に変える
// 終点を変えると自動的に長さにも反映される
// 元の状態で終点だけ変えて長さを変え忘れると範囲として奇妙な値となってしまう
$range->endSec = 12;
echo $range->duration();// 11

 元々全て public にしていた時は長さをインスタンスの外から渡す必要があり、計算もクラスの外のソースコードに記述されていました。duration プロパティを削除して duration メソッドとすることによって長さプロパティの値と始点と終点の差が不一致になる様な異常な状態になることを防げます。

 しかしながらプロパティをメソッドに変えたことによってプロパティのみのクラスよりも速度上の問題が発生しやすくなっています。これは次の様な処理が実行された場合に起きます。

// duration メソッドを呼ぶ度に処理(計算)が走り、
// 単に値を参照するよりも遅くなってしまう
$range->duration();
$range->duration();
$range->duration();
$range->duration();
$range->duration();

 連続して呼ばれる際、常に参照のみで済んでいた冒頭の実装よりも都度計算することになり処理全体にかかる時間が長くなります。例の様に連続で書かれることはなかなかありませんがループはよくあります。これがネックとなっている時は特に高速化が望まれます。

 この複数回呼ばれた際の処理の高速化方法としては値のキャッシュ化が考えられます。 プロパティそのものは全て用意し、それらをクラス内だけで見えるに持ち、適宜必要な計算のみを行います。この実装は例えば次です。

<?php

class TimeRange
{
    private int $startSec; // 開始時刻(秒)
    private int $endSec; // 終了時刻(秒)
    private int $cacheDuration; // 長さ

    public function __construct(int $startSec, int $endSec)
    {
        $this->startSec = $startSec;
        $this->endSec   = $endSec;
        $this->recalculateDuration();// 終点と始点が揃った時に長さを初期化
    }

    /** 長さ */
    public function getDuration(): int
    {
        // 長さプロパティの値を直に返します。参照するのみなので、処理が高速になります。
        return $this->cacheDuration;
    }

    /** 長さを現在の終点と始点で更新します。 */
    protected function recalculateDuration(): void{
        $this->cacheDuration = $this->endSec - $this->startSec;
    }

    // 外部から始点、終点の値が与えられる度に、長さを再計算します。

    public function setStartSec(int $startSec): void
    {
        $this->startSec = $startSec;
        $this->recalculateDuration();
    }

    public function setEndSec(int $endSec): void
    {
        $this->endSec = $endSec;
        $this->recalculateDuration();
    }

    public function getStartSec(): int
    {
        return $this->startSec;
    }

    public function getEndSec(): int
    {
        return $this->endSec;
    }
}

// 1秒目から5秒目までの長さ4秒の範囲
$range = new TimeRange(1, 5);
echo $range->getDuration();// 4 // 長さを得られます。

// 終点を変えると自動的に長さにも反映されます。
// 元の状態で終点だけ変えて長さを変え忘れると範囲として奇妙な値になります。
$range->setEndSec(12);
echo $range->getDuration();// 11


// getDuration メソッドを何度呼んでも
// 都度 $this->cacheDuration が参照されるのみで高速化されます
$range->getDuration();// 11
$range->getDuration();
$range->getDuration();
$range->getDuration();
$range->getDuration();

 内部では全てプロパティとして持ち再定義が必要な際に一意に定まる部分を再定義します。これにより外部は高速で不具合の起きにくいコードを書けるようになります。

 余談ですが JavaScript の React のカスタムフックはこれを楽に作れます。特に再定義部分が楽で、上記例では再定義が必要なタイミングをいちいちソースコード内で直に管理する必要がありますが、useEffect ならば再定義が必要となるトリガーの値を渡すのみで再定義を適宜自動で走らせてくれます。コードは次です。

import React, {useState, useEffect} from 'react';
const useTimeRange = (startSec, endSec) => {
    const [startSec, setStartSec] = useState(startSec);
    const [endSec, setEndSec] = useState(endSec);
    const [duration, setDuration] = useState(endSec - startSec);
    useEffect(() => {
        setDuration(endSec - startSec)
    },[startSec, endSec]);

    return {
        startSec,setStartSec,
        endSec, setEndSec,
        duration
    }
}

 React のライフサイクルにしたがう必要があるため三つの state をを一つのオブジェクトにまとめるべき時も多々ありますが、概ねこの様に記述できます。
 

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG