HSP3.5.1のオブジェクトファイルサイズを徹底的に削減する


# はじめに

hsptvでは，HSPプログラミングコンテストを毎年開催しています．
その中には「hsptv部門」がありますが，この部門に投稿するには，「start.ax」を6000byte以下に収めなければなりません．

投稿した際に苦労したので，記事としてまとめたいと思います．


…という記事を書こうとおもったら先客がいらっしゃったのでリンクを貼ります．

HSPTV部門でのstart.ax削減術
https://qiita.com/hato/items/7b720e87a408a6ab147a

start.ax のダイエット
https://sites.google.com/site/hspsource/diet

HSP: オブジェクトファイルのサイズを小さく
http://freeh.minim.ne.jp/minimini/develop/hspcpt.html

実行ファイルのサイズを小さくする
http://lhsp.s206.xrea.com/hsp_tips5.html

なるべく内容が重ならないようにします…

追記：2019年以降からhsptv部門が消えた。辛い。

# 注意事項

可読性が酷く落ちます

# -

## 共通部分を纏める

共通している部分があったら，変数でまとめてみましょう．以下の例では4byte減ります．

```
	c = cnt*3
	noteget rscore, c	;cnt*3
	noteget ruser, c+1	;cnt*3+1
	noteget rcomm, c+2	;cnt*3+2
```

## 配列をまとめてセットする

```
	dim v, 4
	v.0 = 3
	v.1 = 2
	v.2 = 1
	v.3 = 0
```

と書くと238byteですが，

```
	dim v, 4
	v = 3,2,1,0
```

と書くと166byteになります．
この例のような，1次元配列の場合は`dim`命令も要らないので，154byteまで減らせます．

## hsvcolorを思い出す

HSV形式で色を指定できる`hsvcolor`という命令があります．
HSV形式では，「色相H」「彩度S」「明度V」で色を表現します．

`hsvcolor`でパラメータを省略した場合は`color`と同様に0が指定されます．
HまたはSまたはV成分が0の時，オブジェクトファイルサイズを短縮出来ます．

例えばグレー`RGB=(x,x,x)`の場合，`color x,x,x`となりますが，
`HSV=(0,0,x)`で表現すると `hsvcolor ,,x`と書くことが出来ます．

## マクロの挙動を理解する

`switch`や`for`等はHSPインタプリタの命令では無くマクロによって記述された命令です．

```
for i, 1, 10, 1
	mes "i="+i
next
```

例えば，上のコードは，以下のように展開されます．
`#cmpopt ppout 1`を加えるとファイル`hsptmp.i`が出力されるので，
このファイルの中身からどのように展開されたか確認することが出来ます．

```
 i= 1:*_for_0000:exgoto@hsp  i, 1, 10,*_break_0000
mes@hsp "i="+i
 *_continue_0000: i+= 1:goto@hsp *_for_0000:*_break_0000
```

大量の命令が出てきました．
複雑なマクロを多用すると，ファイルサイズ短縮のチャンスを見逃してしまうかもしれません．

私は単純そうなマクロにも気をつけています．
例えば，`dir_tv`は`dirinfo(5)`に展開されます．命令だけでなくパラメータも1つ隠れていますね．

```
mes dir_tv+"sozai5.jpg
mes dir_tv+"mini_kei.png"
mes dir_tv+"mini_tamane.png"
mes dir_tv+"tamadot.png"
```

「共通部分を纏める」と同様に，一時変数`_dir_tv`に放り込んでから使うようにすることで，
上の例では294byteから270byteに短縮します．

```
_dir_tv = dir_tv
mes _dir_tv+"sozai5.jpg
mes _dir_tv+"mini_kei.png"
mes _dir_tv+"mini_tamane.png"
mes _dir_tv+"tamadot.png"
```

## 浮動小数点数(double)を極力避ける

整数(int)は4byte，浮動小数点数(double)は8byteです．整数に書き換えると4byte減ります．

例えば，次の「たまねちゃんが横倒しの状態から始まり3/4回転するアニメーション」のコードを見てみます．
`celput`の引数の浮動小数点はいくつまで減らせるでしょうか？
`M_PI`が円周率(もちろん浮動小数点数)のマクロであることも忘れずに．

```
#const PATTERN_WID 1

celload dir_tv+"tamadot.png",SOZAI_WID
celdiv PATTERN_WID, 64, 64, 32, 32

repeat 100
	redraw 0: boxf
	pos 100,100

	celput PATTERN_WID, 31, 2,2, M_PI*2.0/4.0 + (M_PI*2.0*(3.0/4.0)/100)*cnt
	
	redraw: wait 1
loop
```

答えは0個．円周率どこいったのー！

```
	celput PATTERN_WID, 31, 2,2, 573204 + 659213*cnt
```

以下，求め方（`PI*2/4` が何故`573204`になるのか）について書きます．

要は，`x \ (PI*2)`が目標値に近くなるような整数`x`を求めたいです．（`\`は剰余）
ラジアン以外の数学の知識は必要ありません．ちょっとだけコーディングするだけです．

1. 2つの角度の差を求める関数`diffangle(double ang1, double ang2)`を作る．
2. `0`から`999999`の整数の中で，`diffangle(x\(PI*2), 目標値)`が最小となるような値を全探索する．

HSPで書いたコードは以下のようになります．

```
#module
	#defcfunc diffangle double ang1, double ang2
	mod = M_PI*2
	dim a,3
	a.0 = absf((ang1 - ang2 - mod)\mod)
	a.1 = absf((ang1 - ang2      )\mod)
	a.2 = absf((ang1 - ang2 + mod)\mod)
	sortval a
	return a.0
#global

	target = M_PI/2

	bestx = 0
	bestdiff = 999
	
	repeat 1000000
		x = cnt
		d = diffangle(target, x)
		if (d < bestdiff){
			bestdiff = d
			bestx = x
		}
	loop

	mes "answer: "+bestx
	mes "diff: "+strf("%e",bestdiff)
```

先程の`573204`が求まります．


## モジュール展開

ランキング機能を用いる際，`hsptv.as`をインクルードすると思います．

```hsp
#ifndef __hsptv__
#define __hsptv__
#runtime "hsptv"
#regcmd 18
#cmd hsptv_send $00

#module hsptv
#define global HSPTV_RANK_MAX 30

#deffunc hsptv_up int _p1, str _p2, int _p3

	buf=""
	hsptv_send buf,_p1,_p2,_p3
	return

#deffunc hsptv_getrank var _p1, var _p2, var _p3, int _p4

	notesel buf
	i=_p4*3
	noteget _p2, i
	_p1=0+_p2
	noteget _p2, i+1
	noteget _p3, i+2
	noteunsel
	return

#global
#endif
```

本当に必要なのは，次の3行だけです．
それ以外の部分は何のためにあるのかというと，hsptvの機能を分かりやすく安全に使うためのwrapperです．

```
#runtime "hsptv"
#regcmd 18
#cmd hsptv_send $00
```

`hsptv_up` 命令，`hsptv_getrank` 命令が5回10回現れないならば，`#include "hsptv.as"`せずに，直接埋め込めこむことで`#deffunc`分のオブジェクトファイルサイズを削減出来ます．


次の `sample/hsptv/hsptv_test.hsp` を例にあげてみます．`start.ax`のサイズは944byteでした．

```hsp:hsptv_test.hsp
#include "hsptv.as"

	score=1000
	sdim comm,64
	mes "HSPTVデータの更新登録テスト"
	hsptv_up -1,""				; 最初に情報を更新しておく
	gosub *update				; ランキング情報の表示

	pos 500,32:objsize 120,24
	mes "スコア"
	input score
	mes "コメント"
	input comm
	button "更新",*send
	button "終了",*ok
	dialog dirinfo(0)
	stop

*send
	hsptv_up score, comm
	gosub *update
	stop
*ok
	end

*update
	color 255,255,255
	boxf 0,32,500,480			; 背景をクリア
	color 0,0,0
	pos 0,32
	repeat 10				; 上位10位のみ表示
	hsptv_getrank rscore,ruser,rcomm,cnt	; 情報を取得する
	rank=cnt+1
	mes "#"+rank+":"+rscore+"("+ruser+") "
	mes "    コメント:"+rcomm
	loop
	return
```

`#include "hsptv.as"` の代わりに先程の3行を埋め込みます．
`hsptv_send` にはバッファが必要なので，`notesel buf` で宣言します．[^1]
`hsptv_up`を`hsptv_send`に，`hsptv_getrank`を`noteget`に書き換えます．

[^1]: `hsptv_test.hsp`では`notesel` は使われていないので，`buf` の為の1度だけで済みます．別の場所で`notesel` が使われている場合はこの限りではありません．

結果，`start.ax`のサイズは228byte減少し，716byteになりました．

```
#runtime "hsptv"		; hsptv.as
#regcmd 18				;
#cmd hsptv_send $00		;

	notesel buf ; hsptv_send用バッファ

	score=1000
	sdim comm,64
	mes "HSPTVデータの更新登録テスト"
	hsptv_send buf,-1,""	; 情報更新のhsptv_upをhsptv_sendに書き換え
	gosub *update

	pos 500,32:objsize 120,24
	mes "スコア"
	input score
	mes "コメント"
	input comm
	button "更新",*send
	button "終了",*ok
	stop

*send
	hsptv_send buf,score,comm	; 情報更新のhsptv_upをhsptv_sendに書き換え
	gosub *update
	stop
*ok
	end

*update
	color 255,255,255
	boxf 0,32,500,480
	color 0,0,0
	pos 0,32
	repeat 10
	noteget rscore, cnt*3	; hsptv_getrankをnotegetに書き換え
	noteget ruser, cnt*3+1	;
	noteget rcomm, cnt*3+2	;
	rank=cnt+1
	mes "#"+rank+":"+rscore+"("+ruser+") "
	mes "    コメント:"+rcomm
	loop
	return
```

おまけ．もう少し頑張ってみました．
サブルーチンの移動や`strf`の導入等の改善を行って614byteです．
スパゲティ感．

```
#runtime "hsptv"        ; hsptv.as
#regcmd 18              ;
#cmd hsptv_send $00     ;

    notesel buf
    
    score=1000
    comm=""
    mes "HSPTVデータの更新登録テスト"
    hsptv_send buf,-1

    pos 500,32:objsize 120,24
    mes "スコア"
    input score
    mes "コメント"
    input comm
    button "更新",*send
    button "終了",*ok

*update
    hsvcolor ,,255
    boxf
    color 
    pos 0,32
    repeat 10
    	c3 = cnt*3
	    noteget rscore, c3
	    noteget ruser, c3+1
	    noteget rcomm, c3+2
	    mes strf("#%d:%s(%s)",cnt+1,rscore, ruser)
	    mes "    コメント:"+rcomm
    loop
    stop
*send
    hsptv_send buf,score,comm
    goto *update
*ok
    end
```