︿
Top

2014年8月16日 星期六

C# Stopwatch: ElapsedMilliseconds != ElapsedTicks / TimeSpan.TicksPerMillisecond ?

緣起

日前在採用 Stopwatch  計算某段程式所需執行的時間時, 發現 Stopwatch 有一些屬性可以使用: 例如: ElapsedMilliseconds , ElapsedTicks ; 而筆者以前的印象中, TimeSpan 有定義一個 TicksPerMillisecond 的屬性, 其值為 10,000; 但實際執行程式, 卻發現  ElapsedMilliseconds != ElapsedTicks / TimeSpan.TicksPerMillisecond ...


問題呈現

如以下程式段落及執行結果, 很明顯的可以看出:  ElapsedMilliseconds != ElapsedTicks / TimeSpan.TicksPerMillisecond ? 難道微軟的 .NET Framework 有問題?

Stopwatch sw = new Stopwatch();
sw.Start();
Thread.Sleep(2000);    //暫停 2 秒鐘
sw.Stop();

Console.WriteLine("1 ms is equals to {0} time ticks ", TimeSpan.TicksPerMillisecond);
Console.WriteLine("Elapsed time for Thread.Sleep(2000) is {0} raw ticks ", sw.ElapsedTicks);
Console.WriteLine("Elapsed time for Thread.Sleep(2000) is {0} ms ", sw.ElapsedMilliseconds);

問題追蹤

為了找出問題到底出在那裡, 所以連到 .NET Framework 的 Reference Source 查了一下 Stopwatch 這個類別的原始程式碼, 終於抓到元兇了, 原來 Stopwatch 裡 ElaspedTicks 與 
TimeSpan.TicksPerMillisecond 的 ticks 是不同的; 前者是 raw ticks, 後者是 time ticks.

茲將上述的程式碼, 加入如下段落, 把 ticks 與 millisecond 轉換相關參數 IsHighResolution, Frequency 呈現出來.
其實, Frequency 代表的是每秒有多少個 raw ticks; 所以最簡單的方式, 是用 ElaspedTicks / Frequency 取得秒數後; 再用時間單位換算至 ms 或 us 或 ns. 下面這段程式, 也順便算出 '週期', 一個 ticks 代表多少奈秒.

Console.WriteLine("Stopwatch.Frequency={0} Stopwatch.IsHighResolution={1} ", Stopwatch.Frequency, Stopwatch.IsHighResolution);

Console.ForegroundColor = ConsoleColor.Yellow;
// Display the timer frequency and resolution.
if (Stopwatch.IsHighResolution)
{
 Console.WriteLine("Operations timed using the system's high-resolution performance counter.");
}
else
{
 Console.WriteLine("Operations timed using the DateTime class.");
}

long frequency = Stopwatch.Frequency;
Console.WriteLine("  Timer frequency in ticks per second = {0}", frequency);
long nsPerTick = (1000L * 1000L * 1000L) / frequency;
Console.WriteLine("  Timer is accurate within {0} nano-seconds (奈秒) (1/十億 秒)", nsPerTick);
double usPerTick = (1000.0 * 1000.0 ) / frequency; //注意: 這裡要用 1000.0 不然只會出現整數
Console.WriteLine("  Timer is accurate within {0} micro-seconds (微秒) (1/百萬 秒)", usPerTick);
double msPerTick = (1000.0) / frequency;
Console.WriteLine("  Timer is accurate within {0} milli-seconds (亳秒) (1/千 秒)", msPerTick);




可參考以下對原始程式的追蹤, 而找到實際的運作狀況, 請留意 [傑士伯] 的註解說明
(1) 由 ElapsedTicks, ElapsedMilliseconds 的 get 實作方法, 可以看到, 前者是 raw tick, 後者是 time tick; 以本例的換算, 約為 1999 = (4871557* 10000 * 1000 / 2435917) / 10000 毫秒

private const long TicksPerMillisecond = 10000;
private const long TicksPerSecond = TicksPerMillisecond * 1000;     //[傑士伯] 10000 * 1000 = 10^7

//[傑士伯] 由以下這2個屬性, 可以看到其實是不同的, 
//         ElapsedTicks 是執行 GetRawElapsedTicks(); 
//         ElapsedMilliseconds 則是執行 GetElapsedDateTimeTicks()/TicksPerMillisecond
public long ElapsedMilliseconds { 
 get { return GetElapsedDateTimeTicks()/TicksPerMillisecond; }   //[傑士伯](範例) (4871557 * 10000 * 1000 / 2435947) / 10000
}  

public long ElapsedTicks { 
 get { return GetRawElapsedTicks(); }
}

(2) 由 建構子 可以看出初始化的變數值
//[傑士伯] 建構子
static Stopwatch() {                       
    bool succeeded = SafeNativeMethods.QueryPerformanceFrequency(out Frequency);            
    if(!succeeded) {
        IsHighResolution = false; 
        Frequency = TicksPerSecond;
        tickFrequency = 1;
    }
    else {
        IsHighResolution = true;
        tickFrequency = TicksPerSecond;   //[傑士伯] 參考前一段的 private const long TicksPerSecond 定義
        tickFrequency /= Frequency;       //[傑士伯](範例) 筆者的機器 10000 * 1000 / 2435947
    }   
}


(3) 以下是 GetElapsedDateTimeTicks() 的實作
private long GetElapsedDateTimeTicks() {
 long rawTicks = GetRawElapsedTicks();   //[傑士伯] 這個會取得 Raw Ticks, 亦即與 ElapsedTicks 屬性相同
 if( IsHighResolution) {
  // convert high resolution perf counter to DateTime ticks
  double dticks = rawTicks;
  dticks *= tickFrequency;    //[傑士伯](範例) 筆者的機器 4871557 * 10000 * 1000 / 2435947
  return unchecked((long)dticks);                        
 }
 else {
  return rawTicks;
 }
}

總結

經由程式追蹤, 終於發現此 Ticks 非彼 Ticks; 一個是 Raw Ticks, 一個是 Time Ticks; 前者與機器效能計數器有關, 後者才是純粹用來計算時間的; 必須要作區隔, 以免誤解.

這裡順便註記一下時間單位:

  • 毫秒 (milli-second) = 1/千 秒, 即 1/10^3, 簡寫 ms
  • 微秒 (micro-second) = 1/百萬 秒 1/10^6, 簡寫 us
  • 奈秒 (nano-second) = 1/十億 秒 1/10^9, 簡寫 ns


參考文件

版本修訂

2014/08/16  修改第2張圖上方的程式段, 將 usPerTick 及 msPerTick 資料型態改為 double; 同時 1000L 也改為 1000.0, 這樣才能取得正確的小數點後數字.

2014/08/09  首次發文

2 則留言: