« 泰我:バスケットで優勝 | メイン | 戦艦大和 »

2010年3月 2日

2010年うるう年問題

ps3.jpg

ありゃ!ソニーもやってしまいましたか。
「使用している時計機能が2010年をうるう年と認識していました。」
とのこと。

ソニー「も」と書いたのは、笑えない切実な理由から。
私のチームが全く同じミスを犯し、今年をうるう年と誤認識して時計を狂わせてしまうということをやらかしてしまったから。

私の方は事前に分かっていたので、パッチ適用を推進して何とか一昨日~昨日にかけてのXデーを乗り切ったところ。
2/29-3/1は徹夜してました。
ゲームなら一日くらい我慢していればよいが、もう、こっちはハラハラものだった。

うるう年

Webを見まわすと、ソニーは小学生でも出来る簡単な計算を何故間違えたんだ?という論調が多い。幾つか例外の年はあるが、4で割って余り0を求めるだけなのに馬鹿なのか?という流れだ。
念のために正確にうるう年の定義を書くと


  • 西暦年が4で割り切れる年はうるう年
  • ただし、西暦年が100で割り切れる年は平年
  • ただし、西暦年が400で割り切れる年はやはりうるう年

となっている。

馬鹿なのか?という論調の人は単純に以下の式をひっぱり出してきて、何故これを間違えるんだ?と説く。

year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)

いやいや、さすがにそんなミスではないだろう。

今回の話を総合すると、ソフトウェアというより、ハードウェア側のRTC(リアル・タイム・クロック)の問題にも思える。なぜなら、基本的に同じソフトウェアを積むPS3でも古い機種しか問題が発生しないのだという。仮にソフトウェアか、ファームウェアに問題があったとすると、新機種では既知問題として修正が入っていたことになる。
インターネットを介して頻繁にPS3のシステム・ソフトウェアの更新を行っているソニーが、さすがにこの問題を放置するとも考えづらい。

従い私は、これはハードウェアのRTCの問題ではないかとみている。

RTCでのミス

ここから、自分の設計チームの恥ずかしい話になる。

さきほど、ハードウェアのカレンダ/時計であるRTCの話をしたが、パソコンやサーバで使用されるRTCはレガシーIOと呼ばれ、IBM PC/AT互換機時代の仕様をずっと引きずっている。
WindowsもLinuxも、カーネルから直接このRTCにアクセスしているわけで、仕様を勝手に拡張してもいけないし、一切変えられない部分でもある。

従い、一般的なコンピュータのRTCには、今でも「年」は下二ケタ分の8bitしかない。
はて?下二けたで年を表して大騒ぎしたような・・・・。

そう、2000年問題。

2000年問題はソフトウェアが年の下二桁しか使っておらず、それに単純に1900を足して年を示していたために、00になった時に1900年と誤認識するという問題だ。
これは当時ソフトウェアのせいにされていたが、実のところ、ハードウェアのカレンダは未だに下二桁のままだったりする。レガシーIOなので、そう簡単に変えられないのだ。変えてしまうと正しく動作しないOSが続出し、もっと痛い目にあうという事だ。
従い、後は上で動くソフトウェア(OS)が適切に判断、処理しろということなのであろう。

さて、ハードウェア、特にLSIの開発の話に入る。
当時我々はIntel Xeonプロセッサ向けのノースブリッジを開発していたのだが、とある理由から、通常サウスブリッジの中にあるレガシーIOの大半をノースブリッジに埋め込む必要性が出てきた。
RTCもその一つで、カレンダ、時計として動作するために、もちろん、うるう年の処理も入れなければならない。
しかし、実際問題

Leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);

こんな式に相当する剰余回路なんて組み込んだらそれこそ「馬鹿」呼ばわりされるのがオチである。
ソフトウェアでは1行で済む式だが、これを専用のHW回路で組むと膨大な回路が必要になる。従い、LSI設計者はこの式をそのまま回路にするなどということは、決してしないのである。
LSI設計者は常にシンプルな回路を目指して設計する。このとき、部下である設計者はこう考えたようだ。

100の剰余、400の剰余は無視しよう。なぜなら、既に2000年は過ぎており、この条件は100年後の話だ。そんな無駄な回路は入れない。

現実を考慮すればこの判断は正しい。

であれば、4の剰余だけ考えればよいが、剰余回路を組むのは馬鹿げている。どのみち年は下二桁の8bitしかないのだから、もっと単純化できるはずだ。

この判断も正しい。
しかし、彼が設計したのは以下のような回路であった。(HW記述言語のVerilog-HDL)

Leap = ~|Year[1:0];

Nor.jpg

要は、Year[7:0]の8bitのうち、さらに下2ビットだけを見て、00bだったらうるう年だと判断しているのである。NOR回路1個で出来る恐ろしく単純な回路だ。
さて、検証してみよう。
コンピュータ内部では数字を16進数で扱うので、年 - 16進数表記(hを付ける) - 2進数表記(bを付ける)と分かりやすいように書き改めてみた。

00年 - 00h - 0000 0000b - うるう年
01年 - 01h - 0000 0001b - 平年
02年 - 02h - 0000 0010b - 平年
03年 - 03h - 0000 0011b - 平年
04年 - 04h - 0000 0100b - うるう年
05年 - 05h - 0000 0101b - 平年
06年 - 06h - 0000 0110b - 平年
07年 - 07h - 0000 0111b - 平年
08年 - 08h - 0000 1000b - うるう年
09年 - 09h - 0000 1001b - 平年
10年 - 0Ah - 0000 1010b - 平年
11年 - 0Bh - 0000 1011b - 平年
12年 - 0Ch - 0000 1100b - うるう年
13年 - 0Dh - 0000 1101b - 平年
14年 - 0Eh - 0000 1110b - 平年
15年 - 0Fh - 0000 1111b - 平年
16年 - 10h - 0001 0000b - うるう年
17年 - 11h - 0001 0001b - 平年
18年 - 12h - 0001 0010b - 平年
19年 - 13h - 0001 0011b - 平年
20年 - 14h - 0001 0100b - うるう年

この辺で良いだろう。
これは正しそうに見える・・・・・・・

が、駄目なのだ。これだと2010年をうるう年と誤認識するのだ。

なぜならば、先ほど「コンピュータ内部では数値は全て16進数で扱う」と書いた。
唯一の例外がある。それがRTCの日時なのだ。
RTCに限り2進化10進表現(BCD)で扱わなければならないのだ!(*注1)
つまり、9 - 10 と進むとき、16進数的に書けば09h - 0Ahではなく、09h - 10hと大きくジャンプするのだ。
先ほどの表を正しく書き直すと以下になる。

00年 - 00h - 0000 0000b - うるう年
01年 - 01h - 0000 0001b - 平年
02年 - 02h - 0000 0010b - 平年
03年 - 03h - 0000 0011b - 平年
04年 - 04h - 0000 0100b - うるう年
05年 - 05h - 0000 0101b - 平年
06年 - 06h - 0000 0110b - 平年
07年 - 07h - 0000 0111b - 平年
08年 - 08h - 0000 1000b - うるう年
09年 - 09h - 0000 1001b - 平年
   年 - 0Ah - 0000 1010b - BCDでは未使用
   年 - 0Bh - 0000 1011b - BCDでは未使用
   年 - 0Ch - 0000 1100b - BCDでは未使用
   年 - 0Dh - 0000 1101b - BCDでは未使用
   年 - 0Eh - 0000 1110b - BCDでは未使用
   年 - 0Fh - 0000 1111b - BCDでは未使用
10年 - 10h - 0001 0000b - うるう年
11年 - 11h - 0001 0001b - 平年
12年 - 12h - 0001 0010b - 平年
13年 - 13h - 0001 0011b - 平年
14年 - 14h - 0001 0100b - うるう年

おぉ!
なんということだ、今年がうるう年になってしまうではないか?!
さらに、うるう年のはずの2012年が平年になり、平年の2014年に再びうるう年と誤判断・・・・・

PS3が同じミスをしているとは思えないが、ハードウェア開発では回路の単純化を考えるあまり、その使用形態(この場合BCD)をすっかり忘れる場合があるということだ。
ちなみに、BCDを忘れたのはうるう年の判断部分だけだ。それ以外のところは全てBCDで回路が組まれている。
うーん、魔がさしたのか?!

で、最もシンプルで正しい回路は以下だと思う。

Leap = ( (~Year[4] & ~Year[1] ) | (Year[4] & Year[1] ) ) & ~Year[0];

Leap.jpg

もちろん、現実を考慮して2100年のことは考えてもいない(ハードウェアなので、そこまで故障せずに動作しているとは到底思えない)。

IntelのRTC

ちなみに、デファクト・スタンダードであるIntelのサウスブリッジ(ICH)の仕様書は、IntelのWebサイトから誰でもダウンロードできるが、RTCに関しては次のような記述がある。

The leap year determination for adding a 29th day to February does not take into account the end-of-the-century exceptions. The logic simply assumes that all years divisible by 4 are leap years. According to the Royal Observatory Greenwich, years that are divisible by 100 are typically not leap years. In every fourth century (years divisible by 400, like 2000), the 100-year-exception is over-ridden and a leap-year occurs.
Note that the year 2100 will be the first time in which the current RTC implementation would incorrectly calculate the leap-year.


「うるう年検出に関し、世紀末の例外は考慮していない。回路は、単純に4で割り切れる年をうるう年とみなす。グリニッジ天文台によれば、通常100で割り切れる年はうるう年ではない。4世紀ごとに(2000年のように400で割り切れる年)、100年例外は無効化され、うるう年となる。
2100年は、このRTCがうるう年の計算を間違える最初の年となることに注意してください。」

ま、そういうこと。繰り返すが、ハードウェアでまともに

Leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);

なんてインプリをする馬鹿はいないのである。さらに、「year % 4」の部分も実際には、

Leap = ( (~Year[4] & ~Year[1] ) | (Year[4] & Year[1] ) ) & ~Year[0];

なっているだけである。


*注1:実のところ、RTCにはBCD以外にバイナリー・モードも存在している。もちろん、我々の設計したRTCは両方のモードで正しく動作するようになっている。さて、うるう年の判断を誤ったと書いたが、実はバイナリー・モードでは正しい挙動であり、Linuxでは正しく動作することが確かめられている。問題なのは、WindowsはRTCのバイナリー・モードをサポートしていない事だ。

投稿者 abeshin : 2010年3月 2日 22:44

コメント

コメント投稿




   保存しますか?

(書式を変更するような一部のHTMLタグを使うことができます)