2020年11月15日 04:06
月ごとのカレンダーは、多くが5段で収まるが、次のようなパターンで6段になる月もある。
ここで、市販のカレンダーには6段の月を素直に6段で描くものと、5段に収めるために下部の30,31日を23,24日と統合しているものがある。
統合しているものが嫌いだ。
マスの余白に予定など書きづらいとか月の日の配置を図形として捉えられないといった不便があるし、一部分だけ数字が小さい不均一さが美しくなくて嫌だ。
とはいえ統合しないとなると、単純に作ると5段の月と6段の月で必要な幅が異なり、そのままでは5段の月の方に無駄な余白が多く出てしまう。
これを嫌って統合する気持ちも分からなくはない。(もっとも中には余白が大量にあるのに統合しているものもあるが、これは何を考えているのかわからない)
これをいかにして無駄無く自然に収めるか、様々なレイアウトの工夫が考えられる。
・5段の月は前月次月の薄い文字のマスで埋める
別にこれでも構わないと思う。5段に比べて多少狭くはなるが。
・6段の月では縦幅が狭いもの
一番素直な解決策。スペースの有効活用の点で悪くないが、6段の月は他の月よりスペースが少し狭く少し不便であるので不公平感が出る。統合式の下の方にあった大きな不便を月全体の小さな不便に分割したとも言える。
・全て6段の月と同じ幅があるが、前月次月の小さいカレンダーや今月の行事・格言などの入ったマスの配置で余白を吸収するもの
大昔に見て上手いなと思った記憶があるのだが、再現しようとするとどうにも記憶のように上手く決まらない。
・6段の月は上部の月表示に食い込むもの
数年前見たもの。好き。曜日表示の位置がずれるが、特に端の方など見なくても分かるので特に問題はない。曜日表示を無くしてもいいくらいだ。
・下に大きな社名があるやつを見ると、そこ削れよと思う。
常に下に社名があったらそこだけ切り取ったり隠したりもできるがたまに日付があればそうできないので社名を見せるためにもいいかもしれない。
・1日だけ飛び出す段は数字だけの幅にする案。
1マス完全に飛び出すよりスペースを節約できる(マスの半分くらいがメモスペースになっているカレンダーを想定している)。メモは数字の横の余白に書ける。
下側は問題ないが上側だと曜日表示がちょっと面倒なことになりそうだ。「1」は幅が狭いので枠内にもそこそこのメモスペースを確保できるかもしれない。
前月次月の薄い表記のマスにも予定を書きたい時には困る。もっともこの場合の薄いマスは統合式5段ではそもそも無かったところなので統合式に劣るわけではないが。
また、不均一で美しくない。
・数字を斜めに順に下げて配置して同じ日数なら縦幅が同じになるカレンダーを考案した。
見づらい。
・噂では海外では5段に収めるためにはみ出した日を上の隙間に配置することがあるらしい。
なかなか不思議な見た目だが、実に無駄なくスペースが使えている。慣れれば一番よいかもしれない。
辞書にたまにある1行をやや超える内容が直前の行の末尾の余白にはみ出す表記にちょっと似た印象を受ける。
ところで統合されるのはみな下の23/30,24/31なのはなぜだろう。
上の方、つまり1/8,2/9とすれば桁が少なくて済んで数字も大きくできるのに…
…とここまで考えて気づいたが、1月8日や2月9日といった月日に見えてしまうというというのはかなり重大な欠点かもしれない。
その後面白い1日/8日統合の例を発見した。模式的に再現したものがこれだ。
どこにあったかというと、某自治体のゴミ収集日表示。
ゴミ収集は平日に行われているため、平日のマスは広く取りたい一方で土日の列は重要ではない。ここで、どんな月でも平日のある段は5段に収まるので、常に統合せず5段で表示することができる。そしてその割を食って土曜の1日/8日統合という珍しいパターンが出現するわけだ。
1 | 1 2 | 1
2 3 4 5 6 7 8 | 3 4 5 6 7 8 9 | 2 3 4 5 6 7 8
9 10 11 12 13 14 15 | 10 11 12 13 14 15 16 | 9 10 11 12 13 14 15
16 17 18 19 20 21 22 | 17 18 19 20 21 22 23 | 16 17 18 19 20 21 22
23 24 25 26 27 28 29 | 24 25 26 27 28 29 30 | 23 24 25 26 27 28 29
30 | 31 | 30 31
2 3 4 5 6 7 8 | 3 4 5 6 7 8 9 | 2 3 4 5 6 7 8
9 10 11 12 13 14 15 | 10 11 12 13 14 15 16 | 9 10 11 12 13 14 15
16 17 18 19 20 21 22 | 17 18 19 20 21 22 23 | 16 17 18 19 20 21 22
23 24 25 26 27 28 29 | 24 25 26 27 28 29 30 | 23 24 25 26 27 28 29
30 | 31 | 30 31
ここで、市販のカレンダーには6段の月を素直に6段で描くものと、5段に収めるために下部の30,31日を23,24日と統合しているものがある。
統合しているものが嫌いだ。
マスの余白に予定など書きづらいとか月の日の配置を図形として捉えられないといった不便があるし、一部分だけ数字が小さい不均一さが美しくなくて嫌だ。
とはいえ統合しないとなると、単純に作ると5段の月と6段の月で必要な幅が異なり、そのままでは5段の月の方に無駄な余白が多く出てしまう。
これを嫌って統合する気持ちも分からなくはない。(もっとも中には余白が大量にあるのに統合しているものもあるが、これは何を考えているのかわからない)
これをいかにして無駄無く自然に収めるか、様々なレイアウトの工夫が考えられる。
・5段の月は前月次月の薄い文字のマスで埋める
別にこれでも構わないと思う。5段に比べて多少狭くはなるが。
・6段の月では縦幅が狭いもの
一番素直な解決策。スペースの有効活用の点で悪くないが、6段の月は他の月よりスペースが少し狭く少し不便であるので不公平感が出る。統合式の下の方にあった大きな不便を月全体の小さな不便に分割したとも言える。
・全て6段の月と同じ幅があるが、前月次月の小さいカレンダーや今月の行事・格言などの入ったマスの配置で余白を吸収するもの
大昔に見て上手いなと思った記憶があるのだが、再現しようとするとどうにも記憶のように上手く決まらない。
・6段の月は上部の月表示に食い込むもの
数年前見たもの。好き。曜日表示の位置がずれるが、特に端の方など見なくても分かるので特に問題はない。曜日表示を無くしてもいいくらいだ。
・下に大きな社名があるやつを見ると、そこ削れよと思う。
常に下に社名があったらそこだけ切り取ったり隠したりもできるがたまに日付があればそうできないので社名を見せるためにもいいかもしれない。
・1日だけ飛び出す段は数字だけの幅にする案。
1マス完全に飛び出すよりスペースを節約できる(マスの半分くらいがメモスペースになっているカレンダーを想定している)。メモは数字の横の余白に書ける。
下側は問題ないが上側だと曜日表示がちょっと面倒なことになりそうだ。「1」は幅が狭いので枠内にもそこそこのメモスペースを確保できるかもしれない。
前月次月の薄い表記のマスにも予定を書きたい時には困る。もっともこの場合の薄いマスは統合式5段ではそもそも無かったところなので統合式に劣るわけではないが。
また、不均一で美しくない。
・数字を斜めに順に下げて配置して同じ日数なら縦幅が同じになるカレンダーを考案した。
見づらい。
・噂では海外では5段に収めるためにはみ出した日を上の隙間に配置することがあるらしい。
なかなか不思議な見た目だが、実に無駄なくスペースが使えている。慣れれば一番よいかもしれない。
辞書にたまにある1行をやや超える内容が直前の行の末尾の余白にはみ出す表記にちょっと似た印象を受ける。
ところで統合されるのはみな下の23/30,24/31なのはなぜだろう。
上の方、つまり1/8,2/9とすれば桁が少なくて済んで数字も大きくできるのに…
…とここまで考えて気づいたが、1月8日や2月9日といった月日に見えてしまうというというのはかなり重大な欠点かもしれない。
その後面白い1日/8日統合の例を発見した。模式的に再現したものがこれだ。
どこにあったかというと、某自治体のゴミ収集日表示。
ゴミ収集は平日に行われているため、平日のマスは広く取りたい一方で土日の列は重要ではない。ここで、どんな月でも平日のある段は5段に収まるので、常に統合せず5段で表示することができる。そしてその割を食って土曜の1日/8日統合という珍しいパターンが出現するわけだ。
2020年10月21日 21:49
今は別館のサイトの方に昔(2007年)書いた内容を、手直ししたり新たな考察を加えたりしたもの。
元のページはこちらだが、文字コードが指定されていないのでChromeなどの低機能なブラウザでは文字化けして見えないかもしれない。Firefoxをおすすめする。
むかーし、むかし。電子手帳というものがあったそうな。
今でもあるけど、今は「ぴーでーえー」なんて横文字で呼んだりして、過去の「電子手帳」という呼び名の方がカッコよくて自分は好きだ。大体皆PDAが何の略か知らないだろう。なにせ自分も知らないのだ。
(↑と当時書いたのだが、今はPDAも携帯電話に吸収されて無くなってしまった。諸行無常。)
その電子手帳が「ちょっと高価な子供のおもちゃ」になりだした頃の話。
思い出深い電子手帳を紹介しよう。カシオから発売されていた電子手帳だ。
「スーパー電子手帳」シリーズと、何かわからないがたぶん何か違う「スーパー電子手帳Jr.」シリーズ。
男児向け女児向けで様々な種類があり、「PKバトルリーグ」「ツインゴール」「ペットテレパシー」などある。
機能は定番の電話帳・カレンダー(スケジュール帳)・電卓。それに子供向けだから占いやらゲーム。
この電子手帳は7×7ドットのひらがなが4行×12文字表示できる。つまり普通に考えて縦32ドット×横96ドットだ。
ほとんどの表示は8×8単位(漢字は16×16)なのだが、例外がカレンダー。
こんな感じに3×5の数字を使っている。3×5は数字をまともに描ける最小のドット数と言ってよいだろう。
日曜祝日は白黒反転、数字横の点はスケジュールがあることを示す。
ここで疑問に思うことはないだろうか?
そう、カレンダーの最大行数は6行。数字は縦5ドット。つまり5*6+隙間5ドットで35ドットとなり、32ドットの画面をオーバーしてしまう。
それでどうなっているかというと簡単なことで、その分のドットがきちんと用意されている。
しかし極限まで無駄を切り詰めているので、なかなかけったいな形になっており、
こうである。
もう惚れちゃいますね。
ここに、
こうなるわけだ。
ところで、これでは日付の表示に画面全部を使ってしまい、曜日が表示できない。
分かりづらくないかって?
大丈夫。
本体の方に書いてあるから。
さて、ここからが新たな発見である。
この電子手帳、パッケージで画面の総ドット数を誇っているのを過去に見た覚えがある。
探すとこちらに発売元はツクダオリジナルだが見たところ同シリーズの「似顔絵電子手帳カラーリスト」のパッケージが見つかった。
「3118ドット」とある。
数えてみよう。メイン部が縦32×横96、加えてはみ出し部分が(4×9+3)が2個で、3150ドット。
おや、計算が合わない。差は3150-3118=32。32? 縦のドット数と同じだ。
まさか…と別に見つけた写真で数えてみると、確かに横は95ドットしかない。そこも切り詰めていたか。
しかし32ドット減らすと何が嬉しいのだろう。
切りの良い32×96=3072ドットには足りないし、よくあるアイコンのためにコモン線が1本多いタイプで33×96=3168なら32ドット減らさなくても足りる(ドットマトリクス外にアイコンがあるが見たところ16個であり、足りる)。
しかし、考えてみると追加のドットはすべて個別に制御する必要があるわけではない。
表示する図形は30・31の下の方と右の点だけ。
右の数字部は「31」しか表示しないので、字と地の2セグメント。左も、調べてみると「1」の字形は上で想像で書いたようなセリフ付きのものではなくデジタル数字の直線の「1」であったので、0と1を表示するには2セグメントで、地を合わせた3セグメントで済む。4つの点も足して合計わずか9セグメントで足りる。
実際には隣り合っていないドットを1つのセグメントにまとめることはできないので多少多くなるが、下図の左9セグメント右7セグメントでいけそうだ。
このセグメントを制御する配線を考えてみよう。
…と、考えてみたら普通にはできそうにない。
何が問題かというと、この囲まれたセグメント。
ドットマトリクス部から横を1ドット削ることで使えるようになるセグメントは、1seg×32com分。(マトリックスの縦横のコモン線・セグメント線と個々のセグメントが紛らわしいので前者をcom・segと書く)
つまりこれだけではマトリックスにすることはできず、セグメント側を全体を1seg(使い方としてはコモン)に割り当ててcom側で制御するしかない。
これではセグメントの隙間を通しでもしない限り他のセグメントに囲まれたセグメントを個別に制御することができない。
これがcomがもう1本あればseg線を上から伸ばしてできるのだが…
もしくはsegが2本あれば2×Nのマトリックスにできてどうにかなるのだが…
と思ったが、よく考えたらそんなことをしなくてもできた!
囲まれたセグメントは1個だけ。1本だけならcom線も追加ドットに隣り合った最下段の1本を上から伸ばすことができる。
周囲のアイコン含め、配線にも問題がない。
このパターンだとうまく説明がつくことがある。
実物の表示は、YouTubeに動画があり、分かったのだが、
これを見ると、最下段でない日曜日には下に枠が付いていることが分かる。
最下段の数字が白黒反転した時は、ドット数からして下に枠がつかず、不自然である。つまり、下もう1列のドットは付けたくても付けられないのだと考えられる。
comがもう1本あって1com×複数segのパターンであれば、下もう1列は容易に追加できる。
また、追加のドットを下ではなく上に付ければ、白黒反転時に端が切れるのが「30」「31」から「1」「2」になり、日曜を含まず休日表示が起こりにくい点、文字数が少ない点からこちらの方が好ましいと考えられるが、1と2を表示できるパターンは囲まれるセグメントが多く、この作戦は不可能である。0と1の下4ドットだからできたパターンなのだ。
1com×複数segのパターンであれば上に1と2のセグメントも可能だ。
もうひとつ、「1」の字形がセリフ付きでなく縦棒なのも、セリフ付きの形は不可能だったためとも考えられる。縦棒でも特におかしいわけではないが、3×5ドットの数字では1に上下や上だけにセリフを付ける方がよく見る気がする。
なお、segが2本ある場合では、下もう1列が不可、1と2のパターンが不可は同様だが、1に上だけならセリフを付けることができる。
実機は確認していないので合っているかはわからないが、今のところ観察結果を上手く説明できており、なかなか可能性が高いのではないかなと思っている。
元のページはこちらだが、文字コードが指定されていないのでChromeなどの低機能なブラウザでは文字化けして見えないかもしれない。Firefoxをおすすめする。
むかーし、むかし。電子手帳というものがあったそうな。
今でもあるけど、今は「ぴーでーえー」なんて横文字で呼んだりして、過去の「電子手帳」という呼び名の方がカッコよくて自分は好きだ。大体皆PDAが何の略か知らないだろう。なにせ自分も知らないのだ。
(↑と当時書いたのだが、今はPDAも携帯電話に吸収されて無くなってしまった。諸行無常。)
その電子手帳が「ちょっと高価な子供のおもちゃ」になりだした頃の話。
思い出深い電子手帳を紹介しよう。カシオから発売されていた電子手帳だ。
「スーパー電子手帳」シリーズと、何かわからないがたぶん何か違う「スーパー電子手帳Jr.」シリーズ。
男児向け女児向けで様々な種類があり、「PKバトルリーグ」「ツインゴール」「ペットテレパシー」などある。
機能は定番の電話帳・カレンダー(スケジュール帳)・電卓。それに子供向けだから占いやらゲーム。
この電子手帳は7×7ドットのひらがなが4行×12文字表示できる。つまり普通に考えて縦32ドット×横96ドットだ。
ほとんどの表示は8×8単位(漢字は16×16)なのだが、例外がカレンダー。
こんな感じに3×5の数字を使っている。3×5は数字をまともに描ける最小のドット数と言ってよいだろう。
日曜祝日は白黒反転、数字横の点はスケジュールがあることを示す。
ここで疑問に思うことはないだろうか?
そう、カレンダーの最大行数は6行。数字は縦5ドット。つまり5*6+隙間5ドットで35ドットとなり、32ドットの画面をオーバーしてしまう。
それでどうなっているかというと簡単なことで、その分のドットがきちんと用意されている。
しかし極限まで無駄を切り詰めているので、なかなかけったいな形になっており、
こうである。
もう惚れちゃいますね。
ここに、
こうなるわけだ。
ところで、これでは日付の表示に画面全部を使ってしまい、曜日が表示できない。
分かりづらくないかって?
大丈夫。
本体の方に書いてあるから。
さて、ここからが新たな発見である。
この電子手帳、パッケージで画面の総ドット数を誇っているのを過去に見た覚えがある。
探すとこちらに発売元はツクダオリジナルだが見たところ同シリーズの「似顔絵電子手帳カラーリスト」のパッケージが見つかった。
「3118ドット」とある。
数えてみよう。メイン部が縦32×横96、加えてはみ出し部分が(4×9+3)が2個で、3150ドット。
おや、計算が合わない。差は3150-3118=32。32? 縦のドット数と同じだ。
まさか…と別に見つけた写真で数えてみると、確かに横は95ドットしかない。そこも切り詰めていたか。
しかし32ドット減らすと何が嬉しいのだろう。
切りの良い32×96=3072ドットには足りないし、よくあるアイコンのためにコモン線が1本多いタイプで33×96=3168なら32ドット減らさなくても足りる(ドットマトリクス外にアイコンがあるが見たところ16個であり、足りる)。
しかし、考えてみると追加のドットはすべて個別に制御する必要があるわけではない。
表示する図形は30・31の下の方と右の点だけ。
右の数字部は「31」しか表示しないので、字と地の2セグメント。左も、調べてみると「1」の字形は上で想像で書いたようなセリフ付きのものではなくデジタル数字の直線の「1」であったので、0と1を表示するには2セグメントで、地を合わせた3セグメントで済む。4つの点も足して合計わずか9セグメントで足りる。
実際には隣り合っていないドットを1つのセグメントにまとめることはできないので多少多くなるが、下図の左9セグメント右7セグメントでいけそうだ。
このセグメントを制御する配線を考えてみよう。
…と、考えてみたら普通にはできそうにない。
何が問題かというと、この囲まれたセグメント。
ドットマトリクス部から横を1ドット削ることで使えるようになるセグメントは、1seg×32com分。(マトリックスの縦横のコモン線・セグメント線と個々のセグメントが紛らわしいので前者をcom・segと書く)
つまりこれだけではマトリックスにすることはできず、セグメント側を全体を1seg(使い方としてはコモン)に割り当ててcom側で制御するしかない。
これではセグメントの隙間を通しでもしない限り他のセグメントに囲まれたセグメントを個別に制御することができない。
これがcomがもう1本あればseg線を上から伸ばしてできるのだが…
もしくはsegが2本あれば2×Nのマトリックスにできてどうにかなるのだが…
と思ったが、よく考えたらそんなことをしなくてもできた!
囲まれたセグメントは1個だけ。1本だけならcom線も追加ドットに隣り合った最下段の1本を上から伸ばすことができる。
周囲のアイコン含め、配線にも問題がない。
このパターンだとうまく説明がつくことがある。
実物の表示は、YouTubeに動画があり、分かったのだが、
これを見ると、最下段でない日曜日には下に枠が付いていることが分かる。
最下段の数字が白黒反転した時は、ドット数からして下に枠がつかず、不自然である。つまり、下もう1列のドットは付けたくても付けられないのだと考えられる。
comがもう1本あって1com×複数segのパターンであれば、下もう1列は容易に追加できる。
また、追加のドットを下ではなく上に付ければ、白黒反転時に端が切れるのが「30」「31」から「1」「2」になり、日曜を含まず休日表示が起こりにくい点、文字数が少ない点からこちらの方が好ましいと考えられるが、1と2を表示できるパターンは囲まれるセグメントが多く、この作戦は不可能である。0と1の下4ドットだからできたパターンなのだ。
1com×複数segのパターンであれば上に1と2のセグメントも可能だ。
もうひとつ、「1」の字形がセリフ付きでなく縦棒なのも、セリフ付きの形は不可能だったためとも考えられる。縦棒でも特におかしいわけではないが、3×5ドットの数字では1に上下や上だけにセリフを付ける方がよく見る気がする。
なお、segが2本ある場合では、下もう1列が不可、1と2のパターンが不可は同様だが、1に上だけならセリフを付けることができる。
実機は確認していないので合っているかはわからないが、今のところ観察結果を上手く説明できており、なかなか可能性が高いのではないかなと思っている。
2020年08月30日 18:55
ネット上で公開した情報が間違っていて訂正するときなどに、変更前の内容を消して変更後の内容だけにする行為が嫌いなので、今までここでは
誤った文章。直した文章。(yy/mm/dd訂正)
のような表示にしていた。
具体例を挙げればこのようなものだ。(ベースラインPICの注意点より)
しかしこれはこれで、内容を知るためには変更後の文だけを読めばよいので、いちいち変更箇所が示されているのは邪魔だ。
間違った文を見てしまった人にはどこが間違っていたのかを伝えたいが、単に記事の内容を知りたい人にかつて間違っていたことを伝える必要はない。
そんなわけで、これからはCSSで変更前を消し、変更後の内容だけが見えるようにする。
上記の例ではこうなる。
変更前の内容はソースを見れば分かるようになっている。
暇を見て過去の記事の書式を変更していく。
ただ、修正直後は修正箇所が見えていたほうが良いかもしれない。
JavaScriptで経過時間で表示を変えるようにしたい。あとインタラクティブに選んだ版間の差分を表示するようなのもやりたい。
具体例を挙げればこのようなものだ。(ベースラインPICの注意点より)
しかしこれはこれで、内容を知るためには変更後の文だけを読めばよいので、いちいち変更箇所が示されているのは邪魔だ。
間違った文を見てしまった人にはどこが間違っていたのかを伝えたいが、単に記事の内容を知りたい人にかつて間違っていたことを伝える必要はない。
そんなわけで、これからはCSSで変更前を消し、変更後の内容だけが見えるようにする。
上記の例ではこうなる。
変更前の内容はソースを見れば分かるようになっている。
暇を見て過去の記事の書式を変更していく。
ただ、修正直後は修正箇所が見えていたほうが良いかもしれない。
JavaScriptで経過時間で表示を変えるようにしたい。あとインタラクティブに選んだ版間の差分を表示するようなのもやりたい。
Post time :
2020年08月30日 18:55
│Comments(0)
2020年04月29日 02:23
PIC、中でも昔ながらのミッドレンジは演算性能が低いことで有名だ。
ベンチマークでどれほど低い値が出るか気になる。マイコンの性能比較に広く使われているDhrystoneは整数演算だけなので低性能なCPUでも走りそうだ。
そう思ってDhrystoneのソースコードを見てみると…
ミッドレンジPICに5000バイトもメモリは無い。最大でも368バイトだ。
-完-
…とここで諦めてしまうのはつまらない。
よく見てみれば2500個の要素を全て使っているわけではない。アクセスするのはわずか4箇所。これならなんとかなるのではないか。
なんとかなるというのはつまり、Dhrystoneをそのまま走らせることはできないにせよ、なるべく結果に影響の出ない範囲でコードをいじったり、もし本来のコードが走ったらどの程度の速度が出るかを推測することで、それなりに意味のある値を出せるのではないか。
というわけでやってみた。
使うコンパイラはMPLABの標準のCコンパイラ、XC8(v1.44)。無償版なのできっと最適化とかに制限があるだろうが気にしない。
まずは何はともあれコンパイルが通るようにしなければならない。適当に配列要素の数を減らそう。50×50要素を7×6要素に。
ついでにもう1つこっちも減らさないといけない。50要素を25要素に。
また総メモリ量もわりときつく、ちょっと増やすとすぐ割り付けに失敗する。
配列の読み書きのコードは変えていない。
範囲外を読み書きすることになるがそこは自分の足を撃てるC言語。何の問題もない。
他数箇所変更した。
・エラーが出るたび適当にそれっぽいヘッダファイルをinclude
よく分からないまま追加しているがまあ多い分には問題ないだろう。
・NOSTRUCTASSIGN
structassignで「can't generate code for this expression」が出たので、それ用に用意されたコードが使われるよう
これにともないmemcpyがコードに書かれたものが使われるようになったが、ライブラリ側のものと衝突したのでコメントアウトした。
・時間計測のためのTIMEだのなんだのを削除
結果に影響の無い部分なのでバッサリと。
このへんをPICで動くように変更するのは手間なので時間計測はシミュレータで行うことにする。
PICは実行クロック数が確定的なので問題ない。
・mallocができない
のでユーザーズガイドを見ると、
幸いmallocが使われているのは初期化の部分、ベンチマーク本体のループの外だ。直接宣言してしまおう。
ポインタが指す先のメモリがどう用意されたものかはプログラムを走らせる上で違いはないだろう。
おそらく最適化を防ぐために持ってまわった定義をしているのだろうと思われ、そこまでの最適化はきっとされていないだろうと判断する。少なくともポインタ参照はされている。
なお、これで測り終えた後見つけたのだが、別の対処法として、呼び出されると事前に用意したポインタを返す疑似mallocを使っている人がいた。(後述のAVRのコード)
こちらの方がより安全そうなので真似してみたのだが、後述のポインタ幅違いがまた出てどうしてもうまくいかなかった。
諦めて直接宣言のままにした。
・よく分からないエラーが出た
PICでレジスタってなんだろう。RAMとは違うのか。
なんだかよく分からないが2分割したらエラーが消えたのでこれでよしとする。
これで先へ進むようになり、コンパイルが通るか…と思いきや意外なことにプログラムメモリが足りない。結構食うんだなあ。
RAMがミッドレンジ最大の368バイトあるものとして手持ちからPIC16F88を選んだのだが、こいつのプログラムメモリは4kワード。
どうせシミュレータで動かすのだし最大の8kワードある石を適当に選ぶか…と思ったが、コードを見ると大量のprintfがあって容量を食っていそうだ。
状況説明のための出力は不要なので開始と終了を1文字だけ残して消す。
結果出力部分に未使用の変数が消されることを防ぐためのprintfがあるがこちらも極力切り詰める。
例えばこれを
なおprintfから呼ばれるputchの実装は、意味のある出力をするものではなく、シミュレータで適宜止めて出力内容を見るための、引数が消されない最低限のコードにした。
これでビルドが通り、(内容はともかく)動作をするようになった。
出力を確かめると、配列の部分の値が違うのは想定通りだが、他に構造体のポインタ演算あたりの部分も違っている。
調べてみるとおかしいのは例の2分割で代入したところで、16bitのポインタに8bitのポインタを代入している。
この際データが不可解な変化を見せており、その影響でそのあとif文で通らないはずの方を通っている。
XC8ではポインタが8bitと16bitの2種類あるのだが、使い分けは手動ではできず、コード中の全ての代入を認識して自動で最適なものを使い分けてくれるという。
これが判定に失敗して間違った代入をしてしまっているように見える。つまりコンパイラのバグではないか。
ループ外に適当に代入文を書いてみたところ今まで8bitだったところに16bitのポインタが使われるようになり、出力が正しくなった。
そして例の2分割代入部分も元のコードでコンパイルが通るようになった。
配列部分も正しく動作しているか調べよう。
配列のアクセスの部分の数値を書き換えて配列内をアクセスするようにし、正しい結果が出るかを見る。
出た。
また、ここで時間計測し、配列アクセス部以外にかかる時間が変化していないことを確認した。
よって、不正なアドレスをアクセスすることで他の実行結果に影響を与えていることは無いと考えられる。
これで多分配列部分以外は本来のDhrystoneの想定通り動いているだろう。
さて配列の量をいじった事によりベンチマーク結果にどれほどの影響が出ているか考えてみよう。
1次元配列の方の要素数は、(2次元配列を[2][3]にしてメモリを空けた上で)[25][30][35][40][48]を試し、実行時間は一切変化しなかった。
こちらの影響は無いものとみて良いだろう。
2次元配列の要素数を[7][6]から変えて変化を見てみる。
配列の宣言の部分と、typedefの部分。
これを見ると、typedefの方の2次元めを変えると実行時間にやや変化が見られるのに対し、
配列の宣言の方は[2][3]でのみ値が変わるが他は要素数によらず一定である。
本来の[50][50]に近づけられないのが気がかりだが、現状の判断として、配列宣言の要素数を大きくしてもベンチマークの実行時間には影響を与えないと考えることにする。
typedefの方は折角なので本来の[50][50]にしておく。
なおこの要素数だが、変えるとコンパイルに失敗することがかなり多い。
要素数の積が48(メモリ量が96バイト)を越えるとメモリ不足でコンパイルできないのは分かるのだが、次表のようにそれ以下でも値に応じて不規則に失敗する。
(○: コンパイル成功 / ×: コンパイルエラー / ?および表範囲外: 未チェック / 空欄: 96バイト超)
この時のエラーは次のようなもので、調べるとメモリのバンクへの割り付けに失敗しているエラーで、PICにはよくあることのようだ。有料版だと出なくなることもあるらしい。
配列の宣言の方はこれでいいとして、実際にアクセスする部分はどうか。この分をどう考えるかは難しい。
何らかの計算をしてアドレスを求め、配列の範囲外とはいえどこかしらをアクセスしてはいる。
しかし、ここで生成されているコードはどうあっても2500の全ての要素にアクセスすることはできない。
そのような不完全なコードで時間を計ってよいものか。
とはいえ2500要素5000バイト分のアドレスというものはミッドレンジではそもそも存在し得ないのだから、それを計算するコードとしてどのようなものを想定するのかの正解は無い。
考えた末、以下2点を補正することにした。
・乗算
まず、アドレスの生成部分を見ると、1バイト×1バイト=1バイトの乗算ルーチンが呼ばれている。実行時間は100サイクル程度。
これはまずい。50×50要素の配列にアクセスするには積を2バイトで持たなければならない。
試しに2つのchar型の変数を乗算してみると実行時間は値によって変わり、124や178サイクル。
この分を適当に補正することにする。1回あたり50サイクル、配列アクセスは4回あるので計200サイクル追加することにする。
・バンク
5000バイトのメモリは存在しないが、せめて複数バンクにわたるメモリのアドレスを算出するコードは考慮しておきたい。
1バンクあたり96バイトという中途半端な値を使うのはさすがに大変すぎる。
仮に大量のメモリを積むならもっとまともなアクセスができるようにするだろう(というか、実際にPIC18やEnhancedミッドレンジでされている)
悩んだ末、「80バイトや96バイトの容量があるバンクが十分に大量にあり、バンクごとに切りの良い64バイトだけを使う」「バンク選択にかかる時間は考えない」ことを仮定し、以下のコードを書いた。
以上より、実測値5800サイクルに2次元配列アクセスの分を想定した補正値400サイクルを加えた6200サイクルを、ミッドレンジPICにおけるDhrystone1回分の実行時間に相当する値と考える。
これをクロック数に直して24800クロック、1MHz・1秒あたりDhrystone相当値は40.3、1757で割りDMIPS/MHzの推定値は、
0.0229 DMIPSっぽい値/MHz
と得られた。
比較しよう。
16bitや32bitのマイコンの宣伝にはよく公式にDMIPSが書かれている。見ると、比較的性能の低い(インオーダー・シングルコア)のものでも1DMIPS/MHz程度はある。
何十倍も違うとあまり比較にならない。
もっと性能の低い、8bitCPUの値を探そう。
探してみるとそれなりに見つかる。
http://oneweekwonder.blogspot.jp/2013/12/z80-dhrystones.html
Z80。定番だ。
3.5MHzで0.142DMIPSというので、0.04DMIPS/MHz。
http://www.homebrewcpu.com/new_stuff.htm
65(C)02とZ80。6502はファミコンで有名だ。
6502 - 1.02MHz: 37Dhrystone
Z80 - 2.5MHz: 91Dhrystone
というので、
37/1757/1.02 = 0.02DMIPS/MHz
91/1757/2.5 = 0.02DMIPS/MHz
Z80の値が上の半分だ。まあ環境やコンパイラでこの程度の誤差は出るものだろう。
http://ww2.tiki.ne.jp/~maro/AVR/project/bench1/index.html
AVR。PICとよく比較されるマイコンだ。
at90s8515・8MHzで、最速の結果で1.938MIPSとある。0.24DMIPS/MHzだ。
ただしこの人、律儀に2500バイト(?)のために読み書きに3クロックかかる外部RAMを接続している。
(*2500バイトと書いてあるのはたぶん間違いだと思う。C言語的にIntは16bit以上のはず。)
自分のやったように必要な分のメモリだけにするなら外部RAMは要らずもっと速くなる。
4クロックから3クロックに変えるだけで1.659MIPSから1.933MIPSに上がったというのだから、同じ割合で1クロックになれば2.920MIPS・0.365MIPS/MHzに上がる計算になる。
http://www.ecrostech.com/Other/Resources/Dhrystone.htm
AVRもうひとつ。
こちらは内蔵メモリに収まるよう配列要素を半分にするなどしているようだ。先述の疑似mallocはここの記述だ。
ATmega64で、0.328DMIPS/MHz。
上の0.24DMIPS/MHzより速く、推定0.365MIPS/MHzよりは遅い。
調べてみるとat90には乗算命令がなくATmegaにはあるようなので、at90より速いはずだ。してみると上の推定0.365MIPS/MHzは少々怪しいか。
これらの値と今回のPICの推定値を比較してみる。
Z80: 0.02~0.04DMIPS/MHz
Z80はゲームボーイのCPUしか扱ったことがないが、1命令のクロック数は4の倍数で、概ねメモリアクセスの回数×4クロックに一致する。これはPICと似た性能である。
本来のZ80は多少クロック数が異なり、またレジスタが多かったりループ命令があったりするが、大勢に影響は無いだろう。
クロックあたりに行う動作がPICと同程度で、命令がZ80の方が豊富であることから、DMIPS値が1倍~2倍というのはだいたい感覚と合っている。
6502: 0.02DMIPS/MHz
6502はメモリアクセスあたり1クロック+α(内部処理分)といった趣で、Z80よりクロックあたりの性能は高い印象がある。
Z80と同等か低い値というのはちょっと感覚と合わない。まあZ80も2つで2倍の差があるくらいだし、この程度は誤差なのかもしれない。
AVR: 0.24~0.365DMIPS/MHz
Z80や6502と1桁違う。PICの10.5~16倍。感覚的にちょっと多すぎな気もするが、よく考えてみよう。
まずAVRは大半の命令が1命令1クロックで動く。PICは基本的に1命令4クロックなので、命令数では2.6~4倍となる。
ミッドレンジPICとAVRのアーキテクチャを比較すると、
・PICは汎用レジスタが1つのみであり何をやるにもメモリに値を置く必要がある
・PICはプログラムメモリのページ・RAMのバンク切り替えで時間を食う
・PICには乗算命令が無い
・PICは間接アドレッシング用のレジスタが1本しか無いことやメモリのバンク分けのため、C言語に向いていない
・一方AVRは間接アドレッシング用のレジスタが16bit×3本もあり、大変C言語向きである
といったあたりの要因からして、2.6倍くらいは妥当だし4倍もまあありうるのかなと思えてくる。
独自の仮定のもとにそれなりにDMIPSに近いと信じる値を算出し、いくつかの8bit CPUの値と比較して感覚とそれなりに合う結果となったので満足である。
結論に問題があるとすれば、まず一番怪しいのが2次元配列アクセスの補正が正当かどうか。
そしてmalloc除去を始め各所で加えた変更が正当かどうか。
あとはPICでまともにC言語を使ったのは初めてなのでなにか手順にとんでもない間違いがあるかもしれない。
有料版で最適化をかけて別次元の速さになったりしないかも少し心配だ。(「(PRO版なら)最大で60%小さく400%速いコードができていたんだぜ」ってコンパイルするたびに言われるのだ)
コードはgithubに上げたので、何か疑問があれば色々試してみるとよいだろう。
https://github.com/Ikadzuchi/Dhrystone88.X
なお、本稿をほぼ書き終えた段階で公開用にコードを整形したところ、ループ外の不要部を削っただけなのだが、ループの実行時間は5800サイクルから5824サイクルに増えた。面倒なので文中の数値は直さない。
コードの位置が変わることによりページをまたいだりして実行時間がこの程度変化することは十分考えられる。
他にもループ外にテスト用の乗算コードを書いたら7016→6996に短縮したこともあった。(不正な場所を実行していた時なので実行時間は大きく違う)
ベンチマークでどれほど低い値が出るか気になる。マイコンの性能比較に広く使われているDhrystoneは整数演算だけなので低性能なCPUでも走りそうだ。
そう思ってDhrystoneのソースコードを見てみると…
int Arr_2_Glob [50] [50];
ミッドレンジPICに5000バイトもメモリは無い。最大でも368バイトだ。
-完-
…とここで諦めてしまうのはつまらない。
よく見てみれば2500個の要素を全て使っているわけではない。アクセスするのはわずか4箇所。これならなんとかなるのではないか。
なんとかなるというのはつまり、Dhrystoneをそのまま走らせることはできないにせよ、なるべく結果に影響の出ない範囲でコードをいじったり、もし本来のコードが走ったらどの程度の速度が出るかを推測することで、それなりに意味のある値を出せるのではないか。
というわけでやってみた。
使うコンパイラはMPLABの標準のCコンパイラ、XC8(v1.44)。無償版なのできっと最適化とかに制限があるだろうが気にしない。
まずは何はともあれコンパイルが通るようにしなければならない。適当に配列要素の数を減らそう。50×50要素を7×6要素に。
int Arr_2_Glob [7] [6]; //Arr_2_Glob [50] [50];
typedef int Arr_2_Dim [7][6]; //[50] [50];
これが妥当かどうかは後で見ていく。ついでにもう1つこっちも減らさないといけない。50要素を25要素に。
int Arr_1_Glob [25]; //Arr_1_Glob [50];
typedef int Arr_1_Dim [25]; //[50];
int 50個=100バイトはあるんではないかと思うかもしれないが、コンパイラの仕様上1つのオブジェクトは1バンクに収まっていないとならない。また総メモリ量もわりときつく、ちょっと増やすとすぐ割り付けに失敗する。
配列の読み書きのコードは変えていない。
範囲外を読み書きすることになるがそこは自分の足を撃てるC言語。何の問題もない。
他数箇所変更した。
・エラーが出るたび適当にそれっぽいヘッダファイルをinclude
よく分からないまま追加しているがまあ多い分には問題ないだろう。
・NOSTRUCTASSIGN
structassignで「can't generate code for this expression」が出たので、それ用に用意されたコードが使われるよう
#define NOSTRUCTASSIGN
したら動くようになった。これにともないmemcpyがコードに書かれたものが使われるようになったが、ライブラリ側のものと衝突したのでコメントアウトした。
・時間計測のためのTIMEだのなんだのを削除
結果に影響の無い部分なのでバッサリと。
このへんをPICで動くように変更するのは手間なので時間計測はシミュレータで行うことにする。
PICは実行クロック数が確定的なので問題ない。
・mallocができない
のでユーザーズガイドを見ると、
Dynamic memory allocation, (heap-based allocation using malloc, etc.) is not supported on any 8-bit device.とある。
幸いmallocが使われているのは初期化の部分、ベンチマーク本体のループの外だ。直接宣言してしまおう。
ポインタが指す先のメモリがどう用意されたものかはプログラムを走らせる上で違いはないだろう。
おそらく最適化を防ぐために持ってまわった定義をしているのだろうと思われ、そこまでの最適化はきっとされていないだろうと判断する。少なくともポインタ参照はされている。
/*
Next_Ptr_Glob = (Rec_Pointer) malloc (sizeof (Rec_Type));
Ptr_Glob = (Rec_Pointer) malloc (sizeof (Rec_Type));
*/
Rec_Type temp1;
Rec_Type temp2;
Next_Ptr_Glob = &temp1;
Ptr_Glob = &temp2;
Next_Ptr_Glob = (Rec_Pointer) malloc (sizeof (Rec_Type));
Ptr_Glob = (Rec_Pointer) malloc (sizeof (Rec_Type));
*/
Rec_Type temp1;
Rec_Type temp2;
Next_Ptr_Glob = &temp1;
Ptr_Glob = &temp2;
なお、これで測り終えた後見つけたのだが、別の対処法として、呼び出されると事前に用意したポインタを返す疑似mallocを使っている人がいた。(後述のAVRのコード)
こちらの方がより安全そうなので真似してみたのだが、後述のポインタ幅違いがまた出てどうしてもうまくいかなかった。
諦めて直接宣言のままにした。
・よく分からないエラーが出た
*Ptr_Ref_Par = Ptr_Glob->Ptr_Comp;
の部分で、「error: (1466) registers unavailable for code generation of this expression」というエラーが出た。PICでレジスタってなんだろう。RAMとは違うのか。
なんだかよく分からないが2分割したらエラーが消えたのでこれでよしとする。
struct record *temp;
temp = Ptr_Glob->Ptr_Comp;
*Ptr_Ref_Par = temp;
…が、後にこれは必要なくなる。後述。temp = Ptr_Glob->Ptr_Comp;
*Ptr_Ref_Par = temp;
これで先へ進むようになり、コンパイルが通るか…と思いきや意外なことにプログラムメモリが足りない。結構食うんだなあ。
RAMがミッドレンジ最大の368バイトあるものとして手持ちからPIC16F88を選んだのだが、こいつのプログラムメモリは4kワード。
どうせシミュレータで動かすのだし最大の8kワードある石を適当に選ぶか…と思ったが、コードを見ると大量のprintfがあって容量を食っていそうだ。
状況説明のための出力は不要なので開始と終了を1文字だけ残して消す。
結果出力部分に未使用の変数が消されることを防ぐためのprintfがあるがこちらも極力切り詰める。
例えばこれを
printf ("Int_Glob: %d\n", Int_Glob);
printf (" should be: %d\n", 5);
printf ("Bool_Glob: %d\n", Bool_Glob);
printf (" should be: %d\n", 1);
こうする感じで。printf (" should be: %d\n", 5);
printf ("Bool_Glob: %d\n", Bool_Glob);
printf (" should be: %d\n", 1);
printf ("a:%d\n", Int_Glob);
printf ("b:%d\n", Bool_Glob);
こうして切り詰めたら余裕で入った。3kワード弱にまでなった。printf ("b:%d\n", Bool_Glob);
なおprintfから呼ばれるputchの実装は、意味のある出力をするものではなく、シミュレータで適宜止めて出力内容を見るための、引数が消されない最低限のコードにした。
これでビルドが通り、(内容はともかく)動作をするようになった。
出力を確かめると、配列の部分の値が違うのは想定通りだが、他に構造体のポインタ演算あたりの部分も違っている。
調べてみるとおかしいのは例の2分割で代入したところで、16bitのポインタに8bitのポインタを代入している。
この際データが不可解な変化を見せており、その影響でそのあとif文で通らないはずの方を通っている。
XC8ではポインタが8bitと16bitの2種類あるのだが、使い分けは手動ではできず、コード中の全ての代入を認識して自動で最適なものを使い分けてくれるという。
これが判定に失敗して間違った代入をしてしまっているように見える。つまりコンパイラのバグではないか。
ループ外に適当に代入文を書いてみたところ今まで8bitだったところに16bitのポインタが使われるようになり、出力が正しくなった。
そして例の2分割代入部分も元のコードでコンパイルが通るようになった。
配列部分も正しく動作しているか調べよう。
配列のアクセスの部分の数値を書き換えて配列内をアクセスするようにし、正しい結果が出るかを見る。
出た。
また、ここで時間計測し、配列アクセス部以外にかかる時間が変化していないことを確認した。
よって、不正なアドレスをアクセスすることで他の実行結果に影響を与えていることは無いと考えられる。
これで多分配列部分以外は本来のDhrystoneの想定通り動いているだろう。
さて配列の量をいじった事によりベンチマーク結果にどれほどの影響が出ているか考えてみよう。
1次元配列の方の要素数は、(2次元配列を[2][3]にしてメモリを空けた上で)[25][30][35][40][48]を試し、実行時間は一切変化しなかった。
こちらの影響は無いものとみて良いだろう。
2次元配列の要素数を[7][6]から変えて変化を見てみる。
配列の宣言の部分と、typedefの部分。
宣\def | [50][50] | [50][6] | [7][50] | [7][6] | [2][2] |
[7][6] | 5800 | 5693 | 5800 | 5693 | 5571 |
[4][6] | 5800 | 5693 | 5800 | 5693 | 5571 |
[8][3] | 5800 | 5693 | 5800 | 5693 | 5571 |
[2][3] | 5800 | 5800 | 5800 | 5800 | 5571 |
これを見ると、typedefの方の2次元めを変えると実行時間にやや変化が見られるのに対し、
配列の宣言の方は[2][3]でのみ値が変わるが他は要素数によらず一定である。
本来の[50][50]に近づけられないのが気がかりだが、現状の判断として、配列宣言の要素数を大きくしてもベンチマークの実行時間には影響を与えないと考えることにする。
typedefの方は折角なので本来の[50][50]にしておく。
なおこの要素数だが、変えるとコンパイルに失敗することがかなり多い。
要素数の積が48(メモリ量が96バイト)を越えるとメモリ不足でコンパイルできないのは分かるのだが、次表のようにそれ以下でも値に応じて不規則に失敗する。
[*][2] | [*][3] | [*][4] | [*][5] | [*][6] | [*][7] | [*][8] | [*][9] | |
[2][*] | × | ○ | ○ | ○ | × | × | × | × |
[3][*] | × | ○ | × | × | ○ | × | × | × |
[4][*] | × | ○ | × | ○ | ○ | × | × | × |
[5][*] | × | ○ | ○ | ○ | ○ | × | × | × |
[6][*] | ○ | ○ | ○ | ○ | ○ | × | × | |
[7][*] | ○ | ○ | ○ | ○ | ○ | |||
[8][*] | ○ | ○ | ○ | ○ | ○ | |||
[9][*] | ? | ? | ○ | ○ | ||||
[10][*] | ? | ? | ○ | |||||
[11][*] | ? | ? | ○ | |||||
[12][*] | ? | ? | ○ |
この時のエラーは次のようなもので、調べるとメモリのバンクへの割り付けに失敗しているエラーで、PICにはよくあることのようだ。有料版だと出なくなることもあるらしい。
D:\P\xc8\v1.44\sources\common\lwdiv.c:30: error: (1357) fixup overflow storing 0x8E in 1 byte at 0x950/0x2 -> 0x4A8 (dist/default/debug\Dhrystone88.X.debug.obj 120/0x26)
配列の宣言の方はこれでいいとして、実際にアクセスする部分はどうか。この分をどう考えるかは難しい。
何らかの計算をしてアドレスを求め、配列の範囲外とはいえどこかしらをアクセスしてはいる。
しかし、ここで生成されているコードはどうあっても2500の全ての要素にアクセスすることはできない。
そのような不完全なコードで時間を計ってよいものか。
とはいえ2500要素5000バイト分のアドレスというものはミッドレンジではそもそも存在し得ないのだから、それを計算するコードとしてどのようなものを想定するのかの正解は無い。
考えた末、以下2点を補正することにした。
・乗算
まず、アドレスの生成部分を見ると、1バイト×1バイト=1バイトの乗算ルーチンが呼ばれている。実行時間は100サイクル程度。
これはまずい。50×50要素の配列にアクセスするには積を2バイトで持たなければならない。
試しに2つのchar型の変数を乗算してみると実行時間は値によって変わり、124や178サイクル。
char temp01 = 8;
char temp02 = 100;
int temp03 = temp01*temp02;
→124char temp02 = 100;
int temp03 = temp01*temp02;
char temp01 = 38;
char temp02 = 100;
int temp03 = temp01*temp02;
→178char temp02 = 100;
int temp03 = temp01*temp02;
この分を適当に補正することにする。1回あたり50サイクル、配列アクセスは4回あるので計200サイクル追加することにする。
・バンク
5000バイトのメモリは存在しないが、せめて複数バンクにわたるメモリのアドレスを算出するコードは考慮しておきたい。
1バンクあたり96バイトという中途半端な値を使うのはさすがに大変すぎる。
仮に大量のメモリを積むならもっとまともなアクセスができるようにするだろう(というか、実際にPIC18やEnhancedミッドレンジでされている)
悩んだ末、「80バイトや96バイトの容量があるバンクが十分に大量にあり、バンクごとに切りの良い64バイトだけを使う」「バンク選択にかかる時間は考えない」ことを仮定し、以下のコードを書いた。
int address = 0x1234
char addrh = address >> 6;
char addrl = address & 0x3F;
これに50サイクル掛かった。2次元配列のアクセスは4箇所あるので200サイクルを追加する。char addrh = address >> 6;
char addrl = address & 0x3F;
以上より、実測値5800サイクルに2次元配列アクセスの分を想定した補正値400サイクルを加えた6200サイクルを、ミッドレンジPICにおけるDhrystone1回分の実行時間に相当する値と考える。
これをクロック数に直して24800クロック、1MHz・1秒あたりDhrystone相当値は40.3、1757で割りDMIPS/MHzの推定値は、
0.0229 DMIPSっぽい値/MHz
と得られた。
比較しよう。
16bitや32bitのマイコンの宣伝にはよく公式にDMIPSが書かれている。見ると、比較的性能の低い(インオーダー・シングルコア)のものでも1DMIPS/MHz程度はある。
CPU | DMIPS/MHz | 情報ソース |
PIC32MM | 1.53 | http://ww1.microchip.com/downloads/en/DeviceDoc/60001324b.pdf |
RL78 | 1.39 | https://www.renesas.com/ja-jp/products/microcontrollers-microprocessors/rl78/rl78-features.html |
ARM Cortex-M0 | 0.89 (最適化条件を変えれば1.02や1.27) | https://developer.arm.com/products/processors/cortex-m/cortex-m0 |
何十倍も違うとあまり比較にならない。
もっと性能の低い、8bitCPUの値を探そう。
探してみるとそれなりに見つかる。
http://oneweekwonder.blogspot.jp/2013/12/z80-dhrystones.html
Z80。定番だ。
3.5MHzで0.142DMIPSというので、0.04DMIPS/MHz。
http://www.homebrewcpu.com/new_stuff.htm
65(C)02とZ80。6502はファミコンで有名だ。
6502 - 1.02MHz: 37Dhrystone
Z80 - 2.5MHz: 91Dhrystone
というので、
37/1757/1.02 = 0.02DMIPS/MHz
91/1757/2.5 = 0.02DMIPS/MHz
Z80の値が上の半分だ。まあ環境やコンパイラでこの程度の誤差は出るものだろう。
http://ww2.tiki.ne.jp/~maro/AVR/project/bench1/index.html
AVR。PICとよく比較されるマイコンだ。
at90s8515・8MHzで、最速の結果で1.938MIPSとある。0.24DMIPS/MHzだ。
ただしこの人、律儀に2500バイト(?)のために読み書きに3クロックかかる外部RAMを接続している。
(*2500バイトと書いてあるのはたぶん間違いだと思う。C言語的にIntは16bit以上のはず。)
自分のやったように必要な分のメモリだけにするなら外部RAMは要らずもっと速くなる。
4クロックから3クロックに変えるだけで1.659MIPSから1.933MIPSに上がったというのだから、同じ割合で1クロックになれば2.920MIPS・0.365MIPS/MHzに上がる計算になる。
http://www.ecrostech.com/Other/Resources/Dhrystone.htm
AVRもうひとつ。
こちらは内蔵メモリに収まるよう配列要素を半分にするなどしているようだ。先述の疑似mallocはここの記述だ。
ATmega64で、0.328DMIPS/MHz。
上の0.24DMIPS/MHzより速く、推定0.365MIPS/MHzよりは遅い。
調べてみるとat90には乗算命令がなくATmegaにはあるようなので、at90より速いはずだ。してみると上の推定0.365MIPS/MHzは少々怪しいか。
これらの値と今回のPICの推定値を比較してみる。
Z80: 0.02~0.04DMIPS/MHz
Z80はゲームボーイのCPUしか扱ったことがないが、1命令のクロック数は4の倍数で、概ねメモリアクセスの回数×4クロックに一致する。これはPICと似た性能である。
本来のZ80は多少クロック数が異なり、またレジスタが多かったりループ命令があったりするが、大勢に影響は無いだろう。
クロックあたりに行う動作がPICと同程度で、命令がZ80の方が豊富であることから、DMIPS値が1倍~2倍というのはだいたい感覚と合っている。
6502: 0.02DMIPS/MHz
6502はメモリアクセスあたり1クロック+α(内部処理分)といった趣で、Z80よりクロックあたりの性能は高い印象がある。
Z80と同等か低い値というのはちょっと感覚と合わない。まあZ80も2つで2倍の差があるくらいだし、この程度は誤差なのかもしれない。
AVR: 0.24~0.365DMIPS/MHz
Z80や6502と1桁違う。PICの10.5~16倍。感覚的にちょっと多すぎな気もするが、よく考えてみよう。
まずAVRは大半の命令が1命令1クロックで動く。PICは基本的に1命令4クロックなので、命令数では2.6~4倍となる。
ミッドレンジPICとAVRのアーキテクチャを比較すると、
・PICは汎用レジスタが1つのみであり何をやるにもメモリに値を置く必要がある
・PICはプログラムメモリのページ・RAMのバンク切り替えで時間を食う
・PICには乗算命令が無い
・PICは間接アドレッシング用のレジスタが1本しか無いことやメモリのバンク分けのため、C言語に向いていない
・一方AVRは間接アドレッシング用のレジスタが16bit×3本もあり、大変C言語向きである
といったあたりの要因からして、2.6倍くらいは妥当だし4倍もまあありうるのかなと思えてくる。
Conclusion
独自の仮定のもとにそれなりにDMIPSに近いと信じる値を算出し、いくつかの8bit CPUの値と比較して感覚とそれなりに合う結果となったので満足である。
結論に問題があるとすれば、まず一番怪しいのが2次元配列アクセスの補正が正当かどうか。
そしてmalloc除去を始め各所で加えた変更が正当かどうか。
あとはPICでまともにC言語を使ったのは初めてなのでなにか手順にとんでもない間違いがあるかもしれない。
有料版で最適化をかけて別次元の速さになったりしないかも少し心配だ。(「(PRO版なら)最大で60%小さく400%速いコードができていたんだぜ」ってコンパイルするたびに言われるのだ)
コードはgithubに上げたので、何か疑問があれば色々試してみるとよいだろう。
https://github.com/Ikadzuchi/Dhrystone88.X
なお、本稿をほぼ書き終えた段階で公開用にコードを整形したところ、ループ外の不要部を削っただけなのだが、ループの実行時間は5800サイクルから5824サイクルに増えた。面倒なので文中の数値は直さない。
コードの位置が変わることによりページをまたいだりして実行時間がこの程度変化することは十分考えられる。
他にもループ外にテスト用の乗算コードを書いたら7016→6996に短縮したこともあった。(不正な場所を実行していた時なので実行時間は大きく違う)
2020年03月10日 09:37
スーパーマリオシリーズの代表的なアイテムにスーパーキノコ(や1UPキノコ)がある。
今は左のようなデザインで定着しているが初代スーパーマリオブラザーズでは右のデザインだった。
初めて初代のキノコを見たときは何だこりゃと思った。
変わりすぎだろ何があった。
…と、ずっと思っていた。
先日マリオ2を初めてプレイして知ったのだが、まず顔が付いたのはマリオ2だったんだな。
マリオコレクション(今風のデザイン)やマリオDXのおまけ(初代のデザイン)でしかマリオ2は見たことがなかったので知らなかった。
スーパーマリオシリーズの様々な物に顔が付いていることについてはこのように言われている。
ただちょっと不思議なのは、初代の時からパッケージイラストにはマリオの手に持つスーパーキノコと思しきキノコにしっかりと顔が書かれていることだ。地面の花をも差し置いて。
スターやジュゲムの雲には初代から顔があるし、これは物には顔を描くというスタイルは初代からあったのを2で徹底したということか。
にしても。
顔以外も2から3で変わりすぎじゃないか。
…と思っていた。
実はそうでもなかったのである。
まず、実は上の画像には1つ嘘がある。
マリオ1・2の斑点は橙~茶色・マリオ3の斑点は赤だと認識していたのだが(そして実際上の画像もそうなっているが)、斑点の色はどれも同じである。ついでにマリオの服や赤ノコノコの甲羅、スーパー木の葉も同じ色である。
今回キノコの色について調べていて初めて気づいた。
スーパーファミコン以降の真っ赤になった色のイメージや、空の色やキノコの地の色の違いで誤認していたものと思われる。
そして同じ条件で撮られた別のソフトの画像はあまり存在しないことから、気づけなかったのだろう。
実際、先の画像はMarioWikiから取ってきたのだが、見ての通り色は一定していなかった。ファミコンの色はかくも曖昧なものである。
次に地の色が白になった事について。これはアートスタイルの変化に対するハードの制限である。
マリオ3ではキャラクターの絵に(大半は黒の)縁取りが付くようになった。
これにより、縁に1色とられることとなり、残り2色でキノコを描くことになった。
地の黄色を残すか柄(え)の白を残すかの選択になるが、1UPキノコとの統一や他のキャラクターとの兼ね合いを考えると、白にするのが自然だろう。
ちなみにマリオ3の通常の(地上面での)スプライトのパレットは
・プレイヤーキャラ用(色はパワーアップなどで変わる。標準マリオはベージュ/赤茶/黒)
・白/赤茶/黒 (スーパーキノコ、スーパー木の葉、赤ノコノコの甲羅、パタクリボーなど)
・白/緑/黒 (1UPキノコ、緑ノコノコの甲羅、蔓/葉など)
・ベージュ/黄土/黒 (クリボー、ノコノコの顔など)
の4種である。
とはいえ縁取りを付けただけで突然3のデザインになるわけでもなかろう。…と思っていた。
試しに2のキノコに縁取りだけ付けてみよう。
ところで、縁取りの付け方には4連結と8連結がある。
不思議なことに、マリオ3ではほとんどのキャラクターのほとんどの部分は8連結で縁取りされているのだが、キノコは4連結である。
キノコだけ最初に描かれたとか、デザイナーが別とかだろうか。何にせよ4連結で描くことにする。
まずは外形をそのままにして描くのが自然だろう。
すると、妙に痩せた印象になってしまった。縁取りが無ければ物と背景との境が外形だと認識するのに対し、縁取りをすると縁の1pxの中央を外形と認識するからだな。
1pxだけ広げよう。
さて、ファミコンにはスプライトの左右反転機能があるので、左右対称な物体は必要なタイル数が半分で済む。
タイル数は節約したいので、左右対称のデザインにしよう。
とりあえずそのまま左右対称にしてみると、
左はあからさまに2つの大きな斑点が並んでしまって不自然だ。右の方が望みがありそうだな。
くっついてしまった上の斑点を切って、中央のスペースが不自然に空いているので、消えてしまった左のものくらいの大きさの斑点を1個置く。
ほぼマリオ3のキノコになってしまった!
比較してみよう。
特に上部の丸みが完全に一致しているのと、斑点の形の大部分が一致しているあたりがポイントが高い。
縁取りを付けてからここまで制限に従って不自然な部分を直していっただけだ。
ここまできたら後は笠を1px下に下ろし、柄を1px太らせ、斑点を自然に調整するだけ。その程度の変化には特に不思議はない。
マリオはハードの制限から生まれたデザインというのは有名だが、キノコもそうだったのだなあ。
次作のワールドで色数やスプライト数の制限が無くなったにも関わらず3のデザインを(地と斑点の色は反転したが)踏襲、更にこの時左右非対称になったのがその後左右対称に戻り、一番制限のきつかった3のデザインに固定されたというのがなかなか面白いところだ。
今は左のようなデザインで定着しているが初代スーパーマリオブラザーズでは右のデザインだった。
初めて初代のキノコを見たときは何だこりゃと思った。
変わりすぎだろ何があった。
…と、ずっと思っていた。
先日マリオ2を初めてプレイして知ったのだが、まず顔が付いたのはマリオ2だったんだな。
マリオコレクション(今風のデザイン)やマリオDXのおまけ(初代のデザイン)でしかマリオ2は見たことがなかったので知らなかった。
スーパーマリオシリーズの様々な物に顔が付いていることについてはこのように言われている。
雲や草には顔があるように見えるが、これは立体感を出すために書き込まれた線が、顔に見えていただけだった。https://www26.atwiki.jp/gcmatome/pages/3410.html
しかし、これが「怖い」と言われたことから、次回作の『2』では、背景の雲や草にはっきりとした顔が書かれた。
ただちょっと不思議なのは、初代の時からパッケージイラストにはマリオの手に持つスーパーキノコと思しきキノコにしっかりと顔が書かれていることだ。地面の花をも差し置いて。
スターやジュゲムの雲には初代から顔があるし、これは物には顔を描くというスタイルは初代からあったのを2で徹底したということか。
にしても。
顔以外も2から3で変わりすぎじゃないか。
…と思っていた。
実はそうでもなかったのである。
まず、実は上の画像には1つ嘘がある。
マリオ1・2の斑点は橙~茶色・マリオ3の斑点は赤だと認識していたのだが(そして実際上の画像もそうなっているが)、斑点の色はどれも同じである。ついでにマリオの服や赤ノコノコの甲羅、スーパー木の葉も同じ色である。
今回キノコの色について調べていて初めて気づいた。
スーパーファミコン以降の真っ赤になった色のイメージや、空の色やキノコの地の色の違いで誤認していたものと思われる。
そして同じ条件で撮られた別のソフトの画像はあまり存在しないことから、気づけなかったのだろう。
実際、先の画像はMarioWikiから取ってきたのだが、見ての通り色は一定していなかった。ファミコンの色はかくも曖昧なものである。
次に地の色が白になった事について。これはアートスタイルの変化に対するハードの制限である。
マリオ3ではキャラクターの絵に(大半は黒の)縁取りが付くようになった。
これにより、縁に1色とられることとなり、残り2色でキノコを描くことになった。
地の黄色を残すか柄(え)の白を残すかの選択になるが、1UPキノコとの統一や他のキャラクターとの兼ね合いを考えると、白にするのが自然だろう。
ちなみにマリオ3の通常の(地上面での)スプライトのパレットは
・プレイヤーキャラ用(色はパワーアップなどで変わる。標準マリオはベージュ/赤茶/黒)
・白/赤茶/黒 (スーパーキノコ、スーパー木の葉、赤ノコノコの甲羅、パタクリボーなど)
・白/緑/黒 (1UPキノコ、緑ノコノコの甲羅、蔓/葉など)
・ベージュ/黄土/黒 (クリボー、ノコノコの顔など)
の4種である。
とはいえ縁取りを付けただけで突然3のデザインになるわけでもなかろう。…と思っていた。
試しに2のキノコに縁取りだけ付けてみよう。
ところで、縁取りの付け方には4連結と8連結がある。
不思議なことに、マリオ3ではほとんどのキャラクターのほとんどの部分は8連結で縁取りされているのだが、キノコは4連結である。
キノコだけ最初に描かれたとか、デザイナーが別とかだろうか。何にせよ4連結で描くことにする。
まずは外形をそのままにして描くのが自然だろう。
すると、妙に痩せた印象になってしまった。縁取りが無ければ物と背景との境が外形だと認識するのに対し、縁取りをすると縁の1pxの中央を外形と認識するからだな。
1pxだけ広げよう。
さて、ファミコンにはスプライトの左右反転機能があるので、左右対称な物体は必要なタイル数が半分で済む。
タイル数は節約したいので、左右対称のデザインにしよう。
とりあえずそのまま左右対称にしてみると、
左はあからさまに2つの大きな斑点が並んでしまって不自然だ。右の方が望みがありそうだな。
くっついてしまった上の斑点を切って、中央のスペースが不自然に空いているので、消えてしまった左のものくらいの大きさの斑点を1個置く。
ほぼマリオ3のキノコになってしまった!
比較してみよう。
特に上部の丸みが完全に一致しているのと、斑点の形の大部分が一致しているあたりがポイントが高い。
縁取りを付けてからここまで制限に従って不自然な部分を直していっただけだ。
ここまできたら後は笠を1px下に下ろし、柄を1px太らせ、斑点を自然に調整するだけ。その程度の変化には特に不思議はない。
マリオはハードの制限から生まれたデザインというのは有名だが、キノコもそうだったのだなあ。
次作のワールドで色数やスプライト数の制限が無くなったにも関わらず3のデザインを(地と斑点の色は反転したが)踏襲、更にこの時左右非対称になったのがその後左右対称に戻り、一番制限のきつかった3のデザインに固定されたというのがなかなか面白いところだ。