簡體   English   中英

在for循環中倒計時

[英]Counting down in for-loops

我相信(從一些研究讀物中),在for循環中倒數實際上在運行時更有效,更快。 我的完整軟件代碼是C ++

我目前有這個:

for (i=0; i<domain; ++i) {

我的'i'是unsigned resgister int,'domain'也是unsigned int

在for-loop中,i用於遍歷數組,例如

array[i] = do stuff

把它轉換成倒計時會弄亂我的例行程序的預期/正確輸出。

我可以想象答案是微不足道的,但我無法理解它。

更新:'do stuff'不依賴於之前或之后的迭代。 for循環中的計算與i的迭代無關。 (我希望這是有道理的)。

更新:要使用我的for循環實現運行時加速,我是否倒計時,如果是這樣,在刪除我的int時刪除未簽名的部分,或者其他什么方法?

請幫忙。

使用無符號計數器只有一種正向循環方法:

for( i = n; i-- > 0; )
{
    // Use i as normal here
}

這里有一個技巧,對於最后一個循環迭代,你將在循環的頂部有i = 1,i--> 0遍,因為1> 0,然后在循環體中i = 0。 在下一次迭代中,i--> 0失敗,因為i == 0,所以后綴減量在計數器上滾動並不重要。

我知道非常不明顯。

我猜你的后向循環看起來像這樣:

for (i = domain - 1; i >= 0; --i) {

在這種情況下,因為i無符號的 ,所以它總是大於或等於零。 當您遞減一個等於零的無符號變量時,它將回繞到一個非常大的數字。 解決方案是使i簽名,或者更改for循環中的條件,如下所示:

for (i = domain - 1; i >= 0 && i < domain; --i) {

或者從domain計數到1而不是從domain - 1計數domain - 10

for (i = domain; i >= 1; --i) {
    array[i - 1] = ...; // notice you have to subtract 1 from i inside the loop now
}

這不是您問題的答案,因為您似乎沒有問題。

這種優化完全不相關,應留給編譯器(如果完成的話)。

您是否已分析過您的程序以檢查您的for循環是否是瓶頸? 如果沒有,那么你不需要花時間擔心這一點。 更重要的是,在你寫作時,將“i”作為“寄存器”int,從性能的角度來看並沒有真正的意義。

即使不知道你的問題域,我也可以向你保證,反向循環技術和“寄存器”int計數器對你的程序性能的影響可以忽略不計 請記住,“過早優化是所有邪惡的根源”。

也就是說,更好地利用優化時間將考慮整體程序結構,使用的數據結構和算法,資源利用率等。

檢查數字是否為零可以比比較更快或更有效。 但這是你真正不應該擔心的那種微優化 - 幾個時鍾周期將與任何其他性能問題相比相形見絀。

在x86上:

dec eax
jnz Foo

代替:

inc eax
cmp eax, 15
jl Foo

如果你有一個不錯的編譯器,它將優化“向上計數”和“倒計時”一樣有效。 試試幾個基准,你會看到。

所以你“讀”了下來更有效率? 除非你向我展示一些分析器結果和代碼,否則我覺得很難相信。 我可以在某些情況下購買它,但在一般情況下,沒有。 在我看來這是一個過早優化的經典案例。

您對“register int i”的評論也很有說服力。 如今,編譯器總是比你更了解如何分配寄存器。 除非您已經分析了代碼,否則不要使用register關鍵字。

當您循環遍歷任何類型的數據結構時,緩存未命中比您前進的方向具有更大的影響。 關注內存布局和算法結構的大局而不是微不足道的微優化。

它與向上向下計數無關。 更快的是朝零 邁克爾的答案顯示了為什么 - x86給出了與零的比較作為許多指令的隱含副作用,因此在調整計數器之后,您只需根據結果進行分支,而不是進行顯式比較。 (也許其他架構也這樣做;我不知道。)

Borland的Pascal編譯器因執行優化而臭名昭着。 編譯器轉換此代碼:

for i := x to y do
  foo(i);

進入內部表示更類似於:

tmp := Succ(y - x);
i := x;
while tmp > 0 do begin
  foo(i);
  Inc(i);
  Dec(tmp);
end;

(我說臭名昭着不是因為優化會影響循環的結果,而是因為調試器錯誤地顯示計數器變量。當程序員檢查i ,調試器可能會顯示tmp的值,導致程序員無法混淆和恐慌誰認為他們的循環正在倒退。)

這個想法是,即使使用額外的IncDec指令,在運行時間方面,它仍然是一個凈贏,而不是進行明確的比較。 你是否真的可以注意到這種差異是有爭議的。

但請注意,轉換是編譯器自動執行的操作,具體取決於它是否認為轉換是有價值的。 編譯器通常比你更好地優化代碼,所以不要花太多精力與它競爭。

無論如何,你問的是C ++,而不是Pascal。 C ++“for”循環不太容易將優化應用於Pascal“for”循環,因為Pascal循環的邊界總是在循環運行之前完全計算,而C ++循環有時依賴於停止條件和循環內容。 C ++編譯器需要進行一些靜態分析,以確定任何給定的循環是否符合Pascal循環有條件無條件轉換的要求。 如果C ++編譯器進行分析,那么它可以進行類似的轉換。

沒有什么可以阻止你自己編寫循環:

for (unsigned i = 0, tmp = domain; tmp > 0; ++i, --tmp)
  array[i] = do stuff

這樣做可能會使您的代碼運行得更快。 就像我之前說過的那樣,你可能不會注意到。 通過手動安排循環來支付的更高成本是您的代碼不再遵循既定慣用語。 你的循環是一個非常普通的“for”循環,但它不再看起來像一個-它有兩個變量,他們在相反的方向計數,其中一個甚至沒有在循環體中使用-因此,任何人讀你的代碼(包括你,一周,一個月或一年后,當你忘記了你希望實現的“優化”)將需要花費額外的努力證明自己循環確實是一個普通的循環變相。

(你是否注意到我上面的代碼使用了無符號變量而沒有繞零的危險?使用兩個單獨的變量允許這樣做。)

從這一切中拿走三件事:

  1. 讓優化器完成它的工作; 總的來說,它比你更好。
  2. 使普通代碼看起來很普通,這樣特殊代碼就不必競爭以獲得人們審閱,調試或維護它的注意力。
  3. 在測試和分析顯示必要之前,不要以性能的名義做任何事情。

您可以嘗試以下方法,哪個編譯器將非常有效地進行優化:

#define for_range(_type, _param, _A1, _B1) \
    for (_type _param = _A1, _finish = _B1,\
    _step = static_cast<_type>(2*(((int)_finish)>(int)_param)-1),\
    _stop = static_cast<_type>(((int)_finish)+(int)_step); _param != _stop; \
_param = static_cast<_type>(((int)_param)+(int)_step))

現在你可以使用它:

for_range (unsigned, i, 10,0)
{
    cout << "backwards i: " << i << endl;
}

for_range (char, c, 'z','a')
{
    cout << c << endl;
}

enum Count { zero, one, two, three }; 

for_range (Count, c, three, zero)
{
    cout << "backwards: " << c << endl;
}

你可以向任何方向迭代:

for_range (Count, c, zero, three)
{
    cout << "forward: " << c << endl;
}

循環

for_range (unsigned,i,b,a)
{
   // body of the loop
}

將產生以下代碼:

 mov esi,b
L1:
;    body of the loop
   dec esi
   cmp esi,a-1
   jne L1 

這里的每個人都專注於表現。 實際上有一個邏輯上的原因是迭代到零,這可以導致更清晰的代碼。

當您通過與數組末尾交換刪除無效元素時,首先迭代最后一個元素是很方便的。 對於不與末尾相鄰的壞元素,我們可以交換到結束位置,減少數組的結束邊界,並繼續迭代。 如果你要迭代到最后,那么交換結束可能會導致交換糟糕的壞事。 通過將end迭代到0,我們知道數組末尾的元素已被證明對此迭代有效。

有關進一步說明......

如果:

  1. 您可以通過交換數組的一端並更改數組邊界來排除壞元素,從而刪除壞元素。

然后很明顯:

  1. 您將與一個好的元素交換,即在此迭代中已經過測試的元素。

所以這意味着:

  1. 如果我們迭代變量bound,那么變量bound和當前迭代指針之間的元素已被證明是好的。 迭代指針是否獲得++或 - 並不重要。 重要的是我們正在迭代變量邊界,因此我們知道與它相鄰的元素是好的。

最后:

  1. 迭代為0允許我們僅使用一個變量來表示數組邊界。 這是否重要是您和編譯器之間的個人決定。

很難說給出的信息,但......反轉你的陣列,並倒計時?

Jeremy Ruten正確地指出使用無符號循環計數器是危險的。 據我所知,這也是不必要的。

其他人也指出了過早優化的危險。 他們是完全正確的。

話雖如此,這是我多年前編程嵌入式系統時使用的一種風格,當時每個字節和每個周期確實都有用。 這些形式具體的CPU和編譯器,我用我有用的,但您的里程可能會有所不同。

// Start out pointing to the last elem in array
pointer_to_array_elem_type p = array + (domain - 1);
for (int i = domain - 1; --i >= 0 ; ) {
     *p-- = (... whatever ...)
}

這種形式利用了在算術運算之后在某些處理器上設置的條件標志 - 在某些體系結構上,分支條件的遞減和測試可以組合成單個指令。 請注意,使用--i--i )是關鍵 - 使用postdecrement( i-- )也不會有效。

或者,

// Start out pointing *beyond* the last elem in array
pointer_to_array_elem_type p = array + domain;
for (pointer_to_array_type p = array + domain; p - domain > 0 ; ) {
     *(--p) = (... whatever ...)
}

第二種形式利用指針(地址)算法。 我很少看到這些天的形式(pointer - int) (有充分的理由),但語言保證當你從指針中減去一個int時,指針會遞減(int * sizeof (*pointer))

我將再次強調,這些形式是否對你來說是一個勝利取決於你正在使用的CPU和編譯器。 他們在摩托羅拉6809和68000架構上為我提供了很好的服務。

在一些后來的arm內核中,遞減和比較只需要一條指令。 這使得遞減循環比遞增循環更有效。

我不知道為什么還沒有增量比較指令。

當這是一個真正的問題時,我很驚訝這篇文章被評為-1。

重要的是,無論你是在增加還是減少你的計數器,無論你是在上升記憶還是記憶力下降。 大多數緩存都針對內存而非內存進行了優化。 由於內存訪問時間是當今大多數程序所面臨的瓶頸,這意味着更改程序以便提高內存可以提高性能,即使這需要將計數器與非零值進行比較。 在我的一些程序中,我通過將代碼更改為內存而不是內存來看到性能的顯着提高。

持懷疑態度? 這是我得到的輸出:

sum up   = 705046256
sum down = 705046256
Ave. Up Memory   = 4839 mus
Ave. Down Memory =  5552 mus
sum up   = inf
sum down = inf
Ave. Up Memory   = 18638 mus
Ave. Down Memory =  19053 mus

從運行這個程序:

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

template<class Iterator, typename T>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, T a, T b) {
  std::random_device rnd_device;
  std::mt19937 generator(rnd_device());
  std::uniform_int_distribution<T> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class Iterator>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, double a, double b) {
  std::random_device rnd_device;
  std::mt19937_64 generator(rnd_device());
  std::uniform_real_distribution<double> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class RAI, class T>
inline void sum_abs_up(RAI first, RAI one_past_last, T &total) {
  T sum = 0;
  auto it = first;
  do {
    sum += *it;
    it++;
  } while (it != one_past_last);
  total += sum;
}

template<class RAI, class T>
inline void sum_abs_down(RAI first, RAI one_past_last, T &total) {
  T sum = 0;
  auto it = one_past_last;
  do {
    it--;
    sum += *it;
  } while (it != first);
  total += sum;
}

template<class T> std::chrono::nanoseconds TimeDown(
                      std::vector<T> &vec, const std::vector<T> &vec_original,
                      std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_down(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class T> std::chrono::nanoseconds TimeUp(
                      std::vector<T> &vec, const std::vector<T> &vec_original,
                      std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_up(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

int main() {
  std::size_t num_repititions = 1 << 10;
  {
  typedef int ValueType;
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(1 << 24);

  FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper);
  const auto vec_original = vec;
  ValueType sum_up = 0, sum_down = 0;

  auto time_up = TimeUp(vec, vec_original, num_repititions, sum_up).count();
  auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count();
  std::cout << "sum up   = " << sum_up   << '\n';
  std::cout << "sum down = " << sum_down << '\n';
  std::cout << "Ave. Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Ave. Down Memory =  "<< time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  }
  {
  typedef double ValueType;
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(1 << 24);

  FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper);
  const auto vec_original = vec;
  ValueType sum_up = 0, sum_down = 0;

  auto time_up = TimeUp(vec, vec_original, num_repititions, sum_up).count();
  auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count();
  std::cout << "sum up   = " << sum_up   << '\n';
  std::cout << "sum down = " << sum_down << '\n';
  std::cout << "Ave. Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Ave. Down Memory =  "<< time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  }
  return 0;
}

sum_abs_upsum_abs_down都做同樣的事情並且以相同的方式定時,唯一的區別是sum_abs_up上升內存而sum_abs_down下降內存。 我甚至通過引用傳遞vec ,以便兩個函數訪問相同的內存位置。 盡管如此, sum_abs_up始終比sum_abs_down快。 自己運行(我用g ++ -O3編譯)。

僅供參考我的vec_original進行實驗,以便我更容易地改變sum_abs_upsum_abs_down ,使得它們改變vec而不允許這些改變影響未來的時間。

重要的是要注意我正在計時的循環是多么緊密。 如果一個循環的主體很大,那么它的迭代器是否上升或下降內存可能無關緊要,因為執行循環體的時間可能完全占主導地位。 此外,重要的是要提到一些罕見的循環,記憶有時比上升更快。 但即使有這樣的循環,也很少會出現上升總是比下降慢的情況(與內存上升的循環不同,后者通常總是比等效的內存循環更快;少數幾次它們甚至是40 +%更快)。

關鍵是,根據經驗,如果你有選項,如果循環的體積很小,並且如果讓你的循環上升記憶而不是向下記憶之間沒有什么區別,那么你應該記憶。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM