2015年10月26日
より良いプログラムを書くための究極の奇策 – 「Data first, not code first」
(2015-09-28)by Evan Todd
本記事は、原著者の許諾のもとに翻訳・掲載しております。
(訳注:2015/10/31、いただいた翻訳フィードバックを元に記事を修正いたしました。)
開発者は嫌うでしょう。
ここでは、標準的なコツや策略について書きますが、本当に興味があるのは、別のことです。究極の奇策を見つけたいと思います。策略をひとつずつ試して、プログラミングの聖域に少しでも近づければ良いのですが。
はじめに
私が初めて書いたビデオゲームは、 Ninja Wars (忍者戦争)でした。
そう、これは、画像で埋めたHTMLのtableです。 src
属性を変えることで、動きを実現しています。JavaScriptファイルの冒頭は下記のようになっています。
var x = 314;
var y = 8;
var prevy= 1;
var prevx= 1;
var prevsw= 0;
var row= 304;
var endrow= 142;
var sword= 296;
var yrow = 0;
var yendrow = 186;
var I = 0;
var e = 0;
var counter = 0;
var found = 0;
var esword = 26;
var eprevsw = 8;
var bluehealth = 40;
var redhealth = 40;
var n = 0;
var you = 'ninja';
var bullet = 'sword';
var enemy = 'enemy';
var ebullet = 'enemysword';
var besieged = 0;
var siegecount = 0;
var esiegecount = 0;
var ebesieged = 0;
var healthcount = 0;
var player = 0;
var starcount = 0;
var infortress = false;
var prevyou = you;
var einfortress = false;
var prevenemy = enemy;
var previmg = "";
var prevbullet= "";
var eprevbullet= "";
var randnum = 0;
var randnum2 = 0;
var randnum3 = 0;
var randnum4 = 0;
var buildcount = 0; var
characters = new Array(4);
characters = ['ninja','tank','heli','builder'];
var bullets = new Array(3);
bullets = ['sword','bullet','5cal','sword'];
var echaracters = new Array(3);
echaracters = ['enemy','tank2','eheli','ebuilder'];
var ebullets = new Array(3);
ebullets = ['enemysword','bullet2','e5cal','enemysword'];
var health = new Array(4);
health = [40,30,20,10];
var prevorb = 0;
var prevnum = 0;
見たことのあるコードだと思います。このようなコードでプログラムを書き始めているのは私だけではありません。しかしながら、(プログラミングの定石のように見える) この部分が実は大間違いです。従って、この部分に対する私の提案が、策略その1となります。
策略その1: グローバル変数は悪である
この時点では、グローバル変数がなぜ悪なのか説明することはできません。直観的に違うと感じているのです。
追記:今はグローバル変数に対して否定的ではないことをはっきりと言っておきます。でも、先走ってしまうことになるので、ここでは否定的な観点から話をします。
オブジェクトについてはこの後触れますが、変数は次のようにまとめることができます。
class Ninja
{
int x, y;
int previousX, previousY;
int health = 100;
}
class Sword
{
int x, y;
int previousX, previousY;
int sharpness = 9000;
}
さらにここで継承を使えば、コピー&ペーストの手間を省くことができます。
class Movable
{
int x, y;
int previousX, previousY;
}
class Ninja : public Movable
{
int health = 100;
}
class Sword : public Movable
{
int sharpness = 9000;
}
継承は便利です。次の策略にも使えます。
策略その2:オブジェクト指向プログラミング
オブジェクト指向プログラミングは最高で、昔ながらのビデオゲームの多くのコア部分となっています。Doom 3のコア部分もそうであり、偶然にも オープンソース となっています。
Doom 3には継承が多用されています。信じられない? このゲームのクラス階層のサブセットの一部は以下の通りです。
idClass
idEntity
idAnimatedEntity
idWeapon
idAFEntity_Base
idAFEntity_ClawFourFingers
idAFEntity_Vehicle
idAFEntity_VehicleFourWheels
idAFEntity_VehicleSixWheels
idAFEntity_Gibbable
idAFEntity_WithAttachedHead
idActor
idPlayer
idAI
仮に、あなたがid Softwareの社員だったとします。この継承階層は最初の数カ月は順調に機能するでしょう。そして、ある運命的な月曜日、大惨事が起こります。ボスに呼ばれ、「計画変更だ。プレーヤーを車にする。」と言われます。
階層の idPlayer
と idAFEntitiy_VehicleFourWheels
を見て下さい。大問題その1です。 多く のコードをいじる必要があります。
大問題その2です。ボスが正気に戻り、「プレーヤーを車にする」案を取り消します。その代わりに、全てに砲塔を装備すると言い出しました。車はHalo Warthog(車体後部に機関銃を装備した四輪駆動車)となり、プレーヤーは大型の砲塔を背負います。
ずぼらなプログラマなので、コピー&ペーストの作業を省くために、また継承を使います。しかし、階層を見てください。砲塔のコードはどこに書き込めばよいのでしょうか。 idPlayer
と idAFEntity_VehicleFourWheels
の共通の親は idAFEntity_Base
のみです。
そのため、恐らく idAFEntity_Base
にコードを書き込み、 calledturret_is_active
のブーリアンフラグを追加します。この時、車とプレーヤーのみをTRUEに設定します。これで確かに機能しますが、その結果、あまりにも多くのものがベース階層に書き込まれすぎてしまっています。 idEntity
のソースコードは次のとおりです。
スクロールして見てください。全部見る必要はありません。
class idEntity : public idClass {
public:
static const int MAX_PVS_AREAS = 4;
static const uint32 INVALID_PREDICTION_KEY = 0xFFFFFFFF;
int entityNumber; // index into the entity list
int entityDefNumber; // index into the entity def list
idLinkList<idEntity> spawnNode; // for being linked into spawnedEntities list
idLinkList<idEntity> activeNode; // for being linked into activeEntities list
idLinkList<idEntity> aimAssistNode; // linked into gameLocal.aimAssistEntities
idLinkList<idEntity> snapshotNode; // for being linked into snapshotEntities list
int snapshotChanged; // used to detect snapshot state changes
int snapshotBits; // number of bits this entity occupied in the last snapshot
bool snapshotStale; // Set to true if this entity is considered stale in the snapshot
idStr name; // name of entity
idDict spawnArgs; // key/value pairs used to spawn and initialize entity
idScriptObject scriptObject; // contains all script defined data for this entity
int thinkFlags; // TH_? Flags
int dormantStart; // time that the entity was first closed off from player
bool cinematic; // during cinematics, entity will only think if cinematic is set
renderView_t * renderView; // for camera views from this entity
idEntity * cameraTarget; // any remoteRenderMap shaders will use this
idList< idEntityPtr<idEntity>, TAG_ENTITY > targets; // when this entity is activated these entities entity are activated
int health; // FIXME: do all objects really need health?
struct entityFlags_s {
bool notarget :1; // if true never attack or target this entity
bool noknockback :1; // if true no knockback from hits
bool takedamage :1; // if true this entity can be damaged
bool hidden :1; // if true this entity is not visible
bool bindOrientated :1; // if true both the master orientation is used for binding
bool solidForTeam :1; // if true this entity is considered solid when a physics team mate pushes entities
bool forcePhysicsUpdate :1; // if true always update from the physics whether the object moved or not
bool selected :1; // if true the entity is selected for editing
bool neverDormant :1; // if true the entity never goes dormant
bool isDormant :1; // if true the entity is dormant
bool hasAwakened :1; // before a monster has been awakened the first time, use full PVS for dormant instead of area-connected
bool networkSync :1; // if true the entity is synchronized over the network
bool grabbed :1; // if true object is currently being grabbed
bool skipReplication :1; // don't replicate this entity over the network.
} fl;
int timeGroup;
bool noGrab;
renderEntity_t xrayEntity;
qhandle_t xrayEntityHandle;
const idDeclSkin * xraySkin;
void DetermineTimeGroup( bool slowmo );
void SetGrabbedState( bool grabbed );
bool IsGrabbed();
public:
ABSTRACT_PROTOTYPE( idEntity );
idEntity();
~idEntity();
void Spawn();
void Save( idSaveGame *savefile ) const;
void Restore( idRestoreGame *savefile );
const char * GetEntityDefName() const;
void SetName( const char *name );
const char * GetName() const;
virtual void UpdateChangeableSpawnArgs( const idDict *source );
int GetEntityNumber() const { return entityNumber; }
// clients generate views based on all the player specific options,
// cameras have custom code, and everything else just uses the axis orientation
virtual renderView_t * GetRenderView(); // thinking
virtual void Think();
bool CheckDormant(); // dormant == on the active list, but out of PVS
virtual void DormantBegin(); // called when entity becomes dormant
virtual void DormantEnd(); // called when entity wakes from being dormant
bool IsActive() const;
void BecomeActive( int flags );
void BecomeInactive( int flags );
void UpdatePVSAreas( const idVec3 &pos );
void BecomeReplicated();
// visuals
virtual void Present(); virtual renderEntity_t *GetRenderEntity();
virtual int GetModelDefHandle();
virtual void SetModel( const char *modelname );
void SetSkin( const idDeclSkin *skin );
const idDeclSkin * GetSkin() const;
void SetShaderParm( int parmnum, float value );
virtual void SetColor( float red, float green, float blue );
virtual void SetColor( const idVec3 &color );
virtual void GetColor( idVec3 &out ) const;
virtual void SetColor( const idVec4 &color );
virtual void GetColor( idVec4 &out ) const;
virtual void FreeModelDef();
virtual void FreeLightDef();
virtual void Hide();
virtual void Show();
bool IsHidden() const;
void UpdateVisuals();
void UpdateModel();
void UpdateModelTransform();
virtual void ProjectOverlay( const idVec3 &origin, const idVec3 &dir, float size, const char *material );
int GetNumPVSAreas();
const int * GetPVSAreas();
void ClearPVSAreas();
bool PhysicsTeamInPVS( pvsHandle_t pvsHandle );
// animation
virtual bool UpdateAnimationControllers();
bool UpdateRenderEntity( renderEntity_s *renderEntity,
const renderView_t *renderView );
static bool ModelCallback( renderEntity_s *renderEntity,
const renderView_t *renderView );
virtual idAnimator * GetAnimator(); // returns animator object used by this entity
// sound
virtual bool CanPlayChatterSounds() const;
bool StartSound( const char *soundName, const s_channelType channel, int soundShaderFlags, bool broadcast, int *length );
bool StartSoundShader( const idSoundShader *shader, const s_channelType channel, int soundShaderFlags, bool broadcast, int *length );
void StopSound( const s_channelType channel, bool broadcast ); // pass SND_CHANNEL_ANY to stop all sounds
void SetSoundVolume( float volume );
void UpdateSound();
int GetListenerId() const; idSoundEmitter * GetSoundEmitter() const;
void FreeSoundEmitter( bool immediate );
// entity binding
virtual void PreBind();
virtual void PostBind();
virtual void PreUnbind();
virtual void PostUnbind();
void JoinTeam( idEntity *teammember );
void Bind( idEntity *master, bool orientated );
void BindToJoint( idEntity *master, const char *jointname, bool orientated );
void BindToJoint( idEntity *master, jointHandle_t jointnum, bool orientated );
void BindToBody( idEntity *master, int bodyId, bool orientated );
void Unbind();
bool IsBound() const;
bool IsBoundTo( idEntity *master ) const;
idEntity * GetBindMaster() const;
jointHandle_t GetBindJoint() const;
int GetBindBody() const;
idEntity * GetTeamMaster() const;
idEntity * GetNextTeamEntity() const;
void ConvertLocalToWorldTransform( idVec3 &offset, idMat3 &axis );
idVec3 GetLocalVector( const idVec3 &vec ) const;
idVec3 GetLocalCoordinates( const idVec3 &vec ) const;
idVec3 GetWorldVector( const idVec3 &vec ) const;
idVec3 GetWorldCoordinates( const idVec3 &vec ) const;
bool GetMasterPosition( idVec3 &masterOrigin, idMat3 &masterAxis ) const;
void GetWorldVelocities( idVec3 &linearVelocity, idVec3 &angularVelocity ) const;
// physics
// set a new physics object to be used by this entity
void SetPhysics( idPhysics *phys );
// get the physics object used by this entity
idPhysics * GetPhysics() const;
// restore physics pointer for save games
void RestorePhysics( idPhysics *phys );
// run the physics for this entity
bool RunPhysics();
// Interpolates the physics, used on MP clients.
void InterpolatePhysics( const float fraction );
// InterpolatePhysics actually calls evaluate, this version doesn't.
void InterpolatePhysicsOnly( const float fraction, bool updateTeam = false );
// set the origin of the physics object (relative to bindMaster if not NULL)
void SetOrigin( const idVec3 &org );
// set the axis of the physics object (relative to bindMaster if not NULL)
void SetAxis( const idMat3 &axis );
// use angles to set the axis of the physics object (relative to bindMaster if not NULL)
void SetAngles( const idAngles &ang );
// get the floor position underneath the physics object
bool GetFloorPos( float max_dist, idVec3 &floorpos ) const;
// retrieves the transformation going from the physics origin/axis to the visual origin/axis
virtual bool GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis );
// retrieves the transformation going from the physics origin/axis to the sound origin/axis
virtual bool GetPhysicsToSoundTransform( idVec3 &origin, idMat3 &axis );
// called from the physics object when colliding, should return true if the physics simulation should stop
virtual bool Collide( const trace_t &collision, const idVec3 &velocity );
// retrieves impact information, 'ent' is the entity retrieving the info
virtual void GetImpactInfo( idEntity *ent, int id, const idVec3 &point, impactInfo_t *info );
// apply an impulse to the physics object, 'ent' is the entity applying the impulse
virtual void ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &impulse );
// add a force to the physics object, 'ent' is the entity adding the force
virtual void AddForce( idEntity *ent, int id, const idVec3 &point, const idVec3 &force );
// activate the physics object, 'ent' is the entity activating this entity
virtual void ActivatePhysics( idEntity *ent );
// returns true if the physics object is at rest
virtual bool IsAtRest() const;
// returns the time the physics object came to rest
virtual int GetRestStartTime() const;
// add a contact entity
virtual void AddContactEntity( idEntity *ent );
// remove a touching entity
virtual void RemoveContactEntity( idEntity *ent );
// damage
// returns true if this entity can be damaged from the given origin
virtual bool CanDamage( const idVec3 &origin, idVec3 &damagePoint ) const;
// applies damage to this entity
virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location );
// adds a damage effect like overlays, blood, sparks, debris etc.
virtual void AddDamageEffect( const trace_t &collision, const idVec3 &velocity, const char *damageDefName );
// callback function for when another entity received damage from this entity. damage can be adjusted and returned to the caller.
virtual void DamageFeedback( idEntity *victim, idEntity *inflictor, int &damage );
// notifies this entity that it is in pain
virtual bool Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location );
// notifies this entity that is has been killed
virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location );
// scripting
virtual bool ShouldConstructScriptObjectAtSpawn() const;
virtual idThread * ConstructScriptObject();
virtual void DeconstructScriptObject();
void SetSignal( signalNum_t signalnum, idThread *thread, const function_t *function );
void ClearSignal( idThread *thread, signalNum_t signalnum );
void ClearSignalThread( signalNum_t signalnum, idThread *thread );
bool HasSignal( signalNum_t signalnum ) const;
void Signal( signalNum_t signalnum );
void SignalEvent( idThread *thread, signalNum_t signalnum );
// gui
void TriggerGuis();
bool HandleGuiCommands( idEntity *entityGui, const char *cmds );
virtual bool HandleSingleGuiCommand( idEntity *entityGui, idLexer *src );
// targets
void FindTargets();
void RemoveNullTargets();
void ActivateTargets( idEntity *activator ) const;
// misc
virtual void Teleport( const idVec3 &origin, const idAngles &angles, idEntity *destination );
bool TouchTriggers() const; idCurve_Spline<idVec3> *GetSpline() const;
virtual void ShowEditingDialog();
enum {
EVENT_STARTSOUNDSHADER,
EVENT_STOPSOUNDSHADER,
EVENT_MAXEVENTS
};
// Called on clients in an MP game, does the actual interpolation for the entity.
// This function will eventually replace ClientPredictionThink completely.
virtual void ClientThink( const int curTime, const float fraction, const bool predict );
virtual void ClientPredictionThink();
virtual void WriteToSnapshot( idBitMsg &msg ) const;
void ReadFromSnapshot_Ex( const idBitMsg &msg );
virtual void ReadFromSnapshot( const idBitMsg &msg );
virtual bool ServerReceiveEvent( int event, int time, const idBitMsg &msg );
virtual bool ClientReceiveEvent( int event, int time, const idBitMsg &msg );
void WriteBindToSnapshot( idBitMsg &msg ) const;
void ReadBindFromSnapshot( const idBitMsg &msg );
void WriteColorToSnapshot( idBitMsg &msg ) const;
void ReadColorFromSnapshot( const idBitMsg &msg );
void WriteGUIToSnapshot( idBitMsg &msg ) const;
void ReadGUIFromSnapshot( const idBitMsg &msg );
void ServerSendEvent( int eventId, const idBitMsg *msg, bool saveEvent, lobbyUserID_t excluding = lobbyUserID_t() ) const;
void ClientSendEvent( int eventId, const idBitMsg *msg ) const;
void SetUseClientInterpolation( bool use ) { useClientInterpolation = use; }
void SetSkipReplication( const bool skip ) { fl.skipReplication = skip; }
bool GetSkipReplication() const { return fl.skipReplication; }
bool IsReplicated() const { return GetEntityNumber() < ENTITYNUM_FIRST_NON_REPLICATED; }
void CreateDeltasFromOldOriginAndAxis( const idVec3 & oldOrigin, const idMat3 & oldAxis );
void DecayOriginAndAxisDelta(); uint32 GetPredictedKey() { return predictionKey; }
void SetPredictedKey( uint32 key_ ) { predictionKey = key_; }
void FlagNewSnapshot();
idEntity* GetTeamChain() { return teamChain; }
// It is only safe to interpolate if this entity has received two snapshots.
enum interpolationBehavior_t {
USE_NO_INTERPOLATION,
USE_LATEST_SNAP_ONLY,
USE_INTERPOLATION
};
interpolationBehavior_t GetInterpolationBehavior() const { return interpolationBehavior; }
unsigned int GetNumSnapshotsReceived() const { return snapshotsReceived; }
protected:
renderEntity_t renderEntity; // used to present a model to the renderer
int modelDefHandle; // handle to static renderer model
refSound_t refSound; // used to present sound to the audio engine
idVec3 GetOriginDelta() const { return originDelta; }
idMat3 GetAxisDelta() const { return axisDelta; }
private:
idPhysics_Static defaultPhysicsObj; // default physics object
idPhysics * physics; // physics used for this entity
idEntity * bindMaster; // entity bound to if unequal NULL
jointHandle_t bindJoint; // joint bound to if unequal INVALID_JOINT
int bindBody; // body bound to if unequal -1
idEntity * teamMaster; // master of the physics team
idEntity * teamChain; // next entity in physics team
bool useClientInterpolation; // disables interpolation for some objects (handy for weapon world models)
int numPVSAreas; // number of renderer areas the entity covers
int PVSAreas[MAX_PVS_AREAS]; // numbers of the renderer areas the entity covers
signalList_t * signals;
int mpGUIState; // local cache to avoid systematic SetStateInt
uint32 predictionKey; // Unique key used to sync predicted ents (projectiles) in MP.
// Delta values that are set when the server or client disagree on where the render model should be. If this happens,
// they resolve it through DecayOriginAndAxisDelta()
idVec3 originDelta;
idMat3 axisDelta;
interpolationBehavior_t interpolationBehavior;
unsigned int snapshotsReceived;
private:
void FixupLocalizedStrings();
bool DoDormantTests(); // dormant == on the active list, but out of PVS
// physics // initialize the default physics
void InitDefaultPhysics( const idVec3 &origin, const idMat3 &axis ); // update visual position from the physics
void UpdateFromPhysics( bool moveBack ); // get physics timestep
virtual int GetPhysicsTimeStep() const;
// entity binding
bool InitBind( idEntity *master ); // initialize an entity binding
void FinishBind(); // finish an entity binding
void RemoveBinds(); // deletes any entities bound to this object
void QuitTeam(); // leave the current team
void UpdatePVSAreas();
// events
void Event_GetName();
void Event_SetName( const char *name );
void Event_FindTargets();
void Event_ActivateTargets( idEntity *activator );
void Event_NumTargets();
void Event_GetTarget( float index );
void Event_RandomTarget( const char *ignore );
void Event_Bind( idEntity *master );
void Event_BindPosition( idEntity *master );
void Event_BindToJoint( idEntity *master, const char *jointname, float orientated );
void Event_Unbind();
void Event_RemoveBinds();
void Event_SpawnBind();
void Event_SetOwner( idEntity *owner );
void Event_SetModel( const char *modelname );
void Event_SetSkin( const char *skinname );
void Event_GetShaderParm( int parmnum );
void Event_SetShaderParm( int parmnum, float value );
void Event_SetShaderParms( float parm0, float parm1, float parm2, float parm3 );
void Event_SetColor( float red, float green, float blue );
void Event_GetColor();
void Event_IsHidden();
void Event_Hide();
void Event_Show();
void Event_CacheSoundShader( const char *soundName );
void Event_StartSoundShader( const char *soundName, int channel );
void Event_StopSound( int channel, int netSync );
void Event_StartSound( const char *soundName, int channel, int netSync );
void Event_FadeSound( int channel, float to, float over );
void Event_GetWorldOrigin();
void Event_SetWorldOrigin( idVec3 const &org );
void Event_GetOrigin();
void Event_SetOrigin( const idVec3 &org );
void Event_GetAngles();
void Event_SetAngles( const idAngles &ang );
void Event_SetLinearVelocity( const idVec3 &velocity );
void Event_GetLinearVelocity();
void Event_SetAngularVelocity( const idVec3 &velocity );
void Event_GetAngularVelocity();
void Event_SetSize( const idVec3 &mins, const idVec3 &maxs );
void Event_GetSize();
void Event_GetMins();
void Event_GetMaxs();
void Event_Touches( idEntity *ent );
void Event_SetGuiParm( const char *key, const char *val );
void Event_SetGuiFloat( const char *key, float f );
void Event_GetNextKey( const char *prefix, const char *lastMatch );
void Event_SetKey( const char *key, const char *value );
void Event_GetKey( const char *key );
void Event_GetIntKey( const char *key );
void Event_GetFloatKey( const char *key );
void Event_GetVectorKey( const char *key );
void Event_GetEntityKey( const char *key );
void Event_RestorePosition();
void Event_UpdateCameraTarget();
void Event_DistanceTo( idEntity *ent );
void Event_DistanceToPoint( const idVec3 &point );
void Event_StartFx( const char *fx );
void Event_WaitFrame();
void Event_Wait( float time );
void Event_HasFunction( const char *name );
void Event_CallFunction( const char *name );
void Event_SetNeverDormant( int enable );
void Event_SetGui( int guiNum, const char *guiName);
void Event_PrecacheGui( const char *guiName );
void Event_GetGuiParm(int guiNum, const char *key);
void Event_GetGuiParmFloat(int guiNum, const char *key);
void Event_GuiNamedEvent(int guiNum, const char *event); };
つまり、言いたいのは、コードが長いということです。Physics debrisに至るまでの全てのエンティティにチームという概念があり、殺されてしまうと概念があります。あきらかに理想的とは言えません。
もし、Unityのゲーム開発者なら解決策はお分かりでしょう。コンポーネントです。次のようになります。
Unityのオブジェクトは、機能の継承ではなくコンポーネントのまとまりなのです。これで、先ほどの砲塔の問題を簡単に解決できます。砲塔のコンポーネントをプレーヤーと車のオブジェクトに追加すれば良いのです。
Doom 3にコンポーネントを使用した場合、次のようなコードになります。
idPlayer
idTransform
idHealth
idAnimatedModel
idAnimator
idRigidBody
idBipedalCharacterController
idPlayerController
idAFEntity_VehicleFourWheels
idTransform
idAnimatedModel
idRigidBody
idFourWheelController
...
ここまででわかることとは何でしょうか。
策略その3:概して継承よりコンポジションを好むべし
ここまでの策略を振り返ってみると、グローバル変数はダメ、オブジェクトはいい、コンポーネントはさらにいいとの結果がでました。次に説明することは信じ難いかもしれません。
ちょっとだけ脱線して、どの関数がより速いのかというとても簡単な質問を手掛かりに下層階のパフォーマンスの世界をのぞいてみましょう。
double a(double x)
{
return Math.sqrt(x);
}
static double[] data;
double b(int x)
{
return data[x];
}
多くの複雑性はひとまず忘れて、これら2つの関数がいずれ、それぞれ1つのx86命令にコンパイルされると仮定します。恐らく、関数 a
は sqrtps
にコンパイルされ、関数 b
は lea
(「load effective address」命令)のようなものにコンパイルされるでしょう。
インテルマニュアル によると、 sqrtps
命令には、最新のインテルプロセッサでおよそ14CPUサイクルが浪費されます。では、 lea
命令はどうでしょうか。
答えは、「それは複雑な問題」ということになります。データの書き込みがどこから実行されるかによって変わってくるのです。
レジスタ | 1コア当たり最高40本(だいたい) | 0サイクル | |
L1 | 1コア当たり32KB | 64B行 | 4サイクル |
L2 | 1コア当たり256KB | 64B行 | 11サイクル |
L3 | 6MB | 64B行 | 40-75サイクル |
メインメモリ | 8GB | 4KBページ | 100-300 サイクル |
最後の数字が重要なのです。メインメモリにアクセスするのに100-300サイクル浪費するのです。つまり、どのような時でも、障害となるのは、メモリへのアクセスになるということでしょう。見て分かるように、L1やL2、L3のキャッシュの使用を増やせばこの状況を改善することができます。でもどのようにすれば良いのでしょうか。
では、Doom 3に戻って現実的な例を挙げてみましょう。次は、Doom 3のアップデートループです。
for ( idEntity* ent = activeEntities.Next();
ent != NULL;
ent = ent->activeNode.Next() )
{
if ( g_cinematic.GetBool() && inCinematic && !ent->cinematic )
{
ent->GetPhysics()->UpdateTime( time );
continue;
}
timer_singlethink.Clear();
timer_singlethink.Start();
RunEntityThink( *ent, cmdMgr );
timer_singlethink.Stop();
ms = timer_singlethink.Milliseconds();
if ( ms >= g_timeentities.GetFloat() )
Printf( "%d: entity '%s': %.1f ms\n", time, ent->name.c_str(), ms );
num++;
}
オブジェクト指向の観点からすると、このコードはきれいで汎用性があります。多分 RunEntityThink
が仮想の Think()
メソッドを呼び出すことで、ほとんどのことができるのだろうと思います。とても応用がきくと思います。
おっと、ボスがまた戻ってきました。質問があるようです。
-
実行とは何? どのオブジェクトがアクティブなのかによって答えは変わってきます。なので、正確には分かりません。
-
実行の順番は? 見当がつきません。ゲームをしている際に、オブジェクトがリストに追加されたり削除されたりします。
-
並行処理にするには? 難しいです。オブジェクトは不規則に実行されるので、オブジェクト同士で互いのステートにアクセスしているのかもしれません。スレッドの間で分割してしまうと、何が起きるか予測できません。
簡単に言うと次のとおりです。
しかし、これだけではありません。よく見てみると、オブジェクトはリンクされたリストに格納されています。メモリ内では、次のようになっています。
これではL1やL2、L3のキャッシュが残念なことになっています。リストの最初のアイテムにアクセスするとキャッシュは、「次に欲しいのは近くのアイテムだ」と思います。そして、最初のアクセスが終わると、次の64バイトを引っ張ってきます。しかしその直後、全く別のメモリ領域へと飛ぶため、キャッシュは使おうとした64バイトをクリアし、RAMにある新データにアクセスします。
策略その4:メモリ内のデータを整列させるとパフォーマンスが大幅に向上する
こんな感じです。
for (int i = 0; i < rigid_bodies.length; i++)
rigid_bodies[i].update();
for (int i = 0; i < ai_controllers.length; i++)
ai_controllers[i].update();
for (int i = 0; i < animated_models.length; i++)
animated_models[i].update();
// ...
オブジェクト指向のプログラマは、このコードに怒りを覚えるかもしれません。汎用性が全然足りないからです。ご覧の通り、隣接している配列に対して同じ処理を繰り返し実行しています(ポインタの配列ではありません、念のため)。従って、メモリの中身はこのようになります。
全てが順番に並んでいます。キャッシュは大満足です。おまけに、このバージョンを使えば、ボスから投げかけられた煩わしい質問に、全部答えることができます。今なら私たちは、実行とはどういうことか、どの順番で実行しているかが分かっていますし、並行処理に書き換えるのもずっとやりやすくなったように見えます。
追記:キャッシュの最適化に熱中しすぎないように気を付けてください。最適化は、全てを配列に配置すれば終わりというわけではありません。本稿でこの処理を取り上げたのは、要点の裏付けとするためですが、私の力では、初歩の導入部分を説明することぐらいしかできません。詳細は本稿の最後に示すリンク先を参照してください。
ではそろそろまとめに入ります。究極の奇策とは何でしょう。策略その1からその4(本当はまだまだ続きがあります)の共通点とは何でしょう。
究極の奇策:コードではなくデータを先に
グローバル変数を悪だと考える根拠は何でしょう(策略その1)? ずぼらなデータ設計でもどうにかごまかせるからです。
オブジェクトはなぜ便利なのでしょう(策略その2)? データの整理がやりやすくなるからです。
コンポーネントがさらに便利なのはなぜでしょう(策略その3)? コンポーネントはデータをうまくモデル化していて、現実のデータ構造と調和しやすくなっているからです。
また、データをきちんと整理すればCPUにも喜んでもらえます(策略その4)。
あまりピンと来ないんだけど、策略って例えばどんなもの?
では実例を示しながら説明します。Unityでよく見られる、コンポーネントベースのゲームデザインの例を示します。
1つのコンポーネントが1つのオブジェクトになっています。オブジェクトの中では、上の方にステート変数が列挙されていて、その下に、その変数を使うメソッドが挙げられています。
これはよく練られたデザインの、オブジェクト指向のシステムなので、変数はプライベート変数です。変数にアクセスできるコードは、そのオブジェクト内のメソッドだけです。これを「カプセル化」と言います。
オブジェクトにはそれぞれ、ある程度の複雑性が含まれます。でも恐れることはありません。OOP(オブジェクト指向プログラミング)の世界では、変数のステートをプライベートにしておく限り、複雑性はオブジェクト内にカプセル化されたままで、他のオブジェクトに拡散することはないというのがお約束です。
しかし残念ながら、これは真実ではありません。
訳:お前が座っているのは嘘で固められた玉座だ
2~3個のオブジェクトにアクセスする関数が必要になることが、往々にしてあります。こういう場合、現実的な解決策は次の2つのうちのどちらかです。1つは、あくまでオブジェクト内で完結するように、関数の方を分割する方法です。もう1つは「getなんとか」「setなんとか」のような関数を山ほど書いて、必要なデータにアクセスする方法です。ただし、どちらのソリューションもあまり感心しません。
ここからが真実です。オブジェクトとして表そうとするとあまりうまくいかないものごとが、現実には存在します。そこで私はオブジェクトに代わるパラダイムを提案します。どんなプログラムでも完璧に表現できるパラダイムです。
訳注:入力データ→プロセス→出力データ
データをプロセスから切り離せば、がぜん分かりやすくなります。
オブジェクト指向には、美しいコードを書きやすいという利点があります。複雑性(つまりステート)はカプセル化するという決まりがあるからです。一方、カプセル化には特定の処理方法しか許されない、という性質もあります。以下の図のようなカプセル化が認められてもいいと私は思います。こうすれば問題が起こった現場では、ちゃんと筋が通るのですから。
まとめると、データ構造の設計は、あなたが直面している問題に即した形にすることが大事です。1つのコンセプトを幾つにも分割して、カプセル化したオブジェクトに押し込んではいけません。
それから、関数を書く時は、使用するデータに残すフットプリントをできるだけ小さくします。可能であれば、純粋にステートレスな関数を書いてください。
これが策略です。
結論
本稿に共感できたとすれば、それは私がほとんど以下の文献に頼ってこれを書いたからでしょう。その文献を正直に紹介します。
最後まで読んでくださってありがとうございました。感想があればぜひコメント欄に書き込んでください。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa