ТЕХНИКА ОПТИМИЗАЦИИ ПРОГРАММ

       

восьмой. Сокращения операций обращение к памяти


Основная масса горячих точек теперь, как показывает профилировка, сосредоточена в цикле подсчета контрольной суммы пароля – на него приходится свыше 80% всего времени исполнения программы, из них 50% "съедает" условный переход, замыкающий цикл (Pentium-процессоры страсть как не любят коротких циклов и условных переходов), а остальные 50% расходуются на обращение к кэш-памяти. Тут уместно сделать небольшое пояснение.

Расхожее мнение утверждает, что чтение нерасщепленных данных, находящихся в кэш-памяти занимает всего один такт, – ровно столько же, сколько и чтение содержимого регистра. Это действительно так, но при более пристальном изучении проблемы выясняется, что "одна ячейка за такт", это пропускная способность кэш памяти, а полное время загрузки данных с учетом латентности составляет как минимум три такта. При чтении зависимых данных из кэша (как в нашем случае) полное время доступа к ячейке определяется не пропускной способностью, а его латентностью. К тому, на процессорах семейства P6 установлено всего лишь одно устройство чтения данных и поэтому, даже при благоприятном стечении обстоятельств они могут загружать всего лишь одну ячейку за такт. Напротив, на данные, хранящиеся в регистрах, это ограничение не распространяется.

Таким образом, для увеличения производительности мы должны избавиться от цикла и до минимума сократить количество обращений к памяти. К сожалению, мы не можем эффективно развернуть цикл, поскольку нам заранее неизвестно количество его итераций. Аналогичная ситуация складывается и с переменными: программируя на ассемблере, мы запросто смогли бы разместить парольный буфер в регистрах общего назначения (благо 16-символьный пароль – самый длинный пароль – который реально найти перебором – размешается всего в четырех регистрах, а в остающиеся три регистра вмещаются все остальные переменные). Но для прикладных программистов, владеющих одним лишь языком высокого уровня этот путь закрыт и им приходится искать другие решения.


И такие решения действительно есть! До сих пор мы увеличивали скорость выполнения программы за счет отказа от наиболее "тяжеловесных" операций, практически не меняя базовых вычислительных алгоритмов. Этот путь привел к колоссальному увеличению производительности, но сейчас он себя исчерпал и дальнейшая оптимизация возможна лишь на алгоритмическом
уровне.
В алгоритмической же оптимизации нет и не может быть универсальных советов и общих решений, – каждый случай должен рассматриваться индивидуально, в контексте своего окружения. Возвращаясь к нашим баранам, задумается: а так ли необходимо считать контрольные суммы каждого нового пароля? В силу слабости используемого алгоритма подсчета CRC, его можно заменить другим, – более быстродействующим, но эквивалентным алгоритмом.
Действительно, поскольку младший байт пароля суммируется всего лишь один раз, то при переходе к следующему паролю его контрольная сумма в большинстве случаев увеличивается ровно на единицу. "В большинстве" – потому, что при изменении второго и последующих байтов пароля, изменяемый байт уже суммируется как минимум дважды, к тому же постоянно попадает то в один, то в другой разряд. Это немного усложняет наш алгоритм, но все же не оставляет его далеко впереди "тупой" методики постоянного подсчета контрольной суммы, используемой ранее.
Словом, финальная реализация улучшенного переборщика паролей может выглядеть так:
int gen_pswd(char *crypteddata, char *pswd, int max_iter, int validCRC)
{
       int a, b, x;
       int p = 0;


       char *buff;
       int y=0;
       int k;
       int length = strlen(pswd);
       int mask;
      
       x = -1;
       for (b = 0; b <= length;  b++)
              x += *(int *)((int)pswd + b);
       for(a = 0; a <  max_iter ; a++)
       {
              if (x==validCRC)
              {
              buff = (char *) malloc(strlen(crypteddata));
              strcpy(buff, crypteddata); DeCrypt(pswd, buff);


      
              printf("CRC %8X: try to decrypt: \"%s\"\n", validCRC,buff);
              }
              y = 1;
              k = 'z'-'!';
              while((++pswd[p])>'z')
              {
                     pswd[p] = '!';
                     // следующий символ
                     y = y | y << 8;
                     x -= k;
                     k = k << 8;
                     k += ('z'-'!');
                    
                     p++;
                     if (!pswd[p])
                     {
                                  pswd[p]='!';
                                  pswd[p+1]=0;
                                  length++;
                                  x = -1;
                                  for (b = 0; b <= length;  b++)
                                         x += *(int *)((int)pswd + b);
                                   y = 0;
                                  pswd[p]=' ';
                     }
                     //printf("%x\n",y);
              } // end while(pswd)
              p = 0;
              x+=y;
       } // end for(a)
       return 0;
}
Листинг 19 Алгоритмическая оптимизация алгоритма просчета CRC
Какой результат дала алгоритмическая оптимизация? Ни за что не догадаетесь –восемьдесят три миллиона паролей в секунду или ~1/10 пароля за такт. Фантастика!
И это при том, что программа написана на чистом Си! И ведь самое забавное, что хороший резерв производительности по-прежнему есть!

Содержание раздела