mya parametrami. Pervyj parametr -- eto imya (bol'shogo) fajla dlya chteniya, a vtoroj -- cifra 1, 2 ili 3, vybirayushchaya funkciyu workc(), workcpp() ili work3() sootvetstvenno. Tol'ko ne zabud'te pro diskovyj kesh, t.e. dlya polucheniya ob容ktivnyh rezul'tatov programmu nuzhno zapustit' neskol'ko raz dlya kazhdogo iz variantov.

Neobychnym mestom zdes' yavlyaetsya funkciya work3() i sootvetstvuyushchij ej klass File. Oni napisany special'no dlya proverki "chestnosti" realizacii standartnyh sredstv vvoda-vyvoda C -- FILE*. Esli vdrug okazhetsya, chto workc() rabotaet sushchestvenno medlennee work3(), to vy imeete polnoe pravo nazvat' sozdatelej takoj biblioteki, kak minimum, polnymi neuchami.

A sejchas poprobuem poluchit' informaciyu k razmyshleniyu: provedem seriyu kontrol'nyh zapuskov i posmotrim na rezul'tat.

I chto zhe nam govoryat bezzhalostnye cifry? Raznica v razy! A dlya odnogo shiroko rasprostranennogo kommercheskogo paketa (ne budem pokazyvat' pal'cem) ona poroj dostigala 11 raz!!!

Stoit tol'ko vzglyanut' na opredeleniya vyzyvaemyh funkcij, kak otvet srazu stanet ochevidnym.

Dlya C s ego getc() v tipichnoj realizacii my imeem:

#define getc(f) ((--((f)->level) >= 0) ? (unsigned char)(*(f)->curp++) : _fgetc (f))
T.e. koroten'kij makros vmesto funkcii. Kak govoritsya -- vsego-nichego. A vot dlya C++ standart trebuet stol'ko, chto ocherednoj raz zadaesh'sya voprosom: dumali li gospoda-komitetchiki o tom, chto gor'kie plody ih tvorchestva komu-to real'no pridetsya primenyat'?!

Nu i ladno: preduprezhden -- vooruzhen! A chto, esli zadat' bufer pobol'she?

void workc(char* fn)
{
 // ...

 if (setvbuf(fil, 0, _IOFBF, LARGE_BUFSIZ)) return;

 // ...
}

void workcpp(char* fn)
{
 // ...

 char* buf=new char[LARGE_BUFSIZ];
 fil.rdbuf()->pubsetbuf(buf, LARGE_BUFSIZ);

 // ...

 delete [] buf;
}
Kak ni stranno, po suti nichego ne izmenitsya! Delo v tom, chto sovremennye OS pri rabote s diskom ispol'zuyut ochen' kachestvennye algoritmy keshirovaniya, tak chto eshche odin uroven' buferizacii vnutri prilozheniya okazyvaetsya izlishnim (v tom smysle, chto ispol'zuemye po umolchaniyu bufery potokov vpolne adekvatny).

Kstati, odnim iz horoshih primerov neobhodimosti ispol'zovaniya mnogopotochnyh programm yavlyaetsya vozmozhnost' uskoreniya raboty programm kopirovaniya fajlov, kogda ishodnyj fajl i kopiya raspolozheny na raznyh ustrojstvah. V etom sluchae programma zapuskaet neskol'ko potokov, osushchestvlyayushchih asinhronnye chtenie i zapis'. No v sovremennyh OS v etom net nikakogo smysla, t.k. predostavlyaemoe sistemoj keshirovanie krome vsego prochego obespechivaet i prozrachnoe dlya prikladnyh programm asinhronnoe chtenie i zapis'.

Podvodya itog, hochetsya otmetit', chto esli vvod-vyvod yavlyaetsya uzkim mestom vashego prilozheniya, to sleduet vozderzhat'sya ot ispol'zovaniya standartnyh potokov C++ i ispol'zovat' proverennye desyatiletiyami metody.


Str.701: 21.4.6.3. Manipulyatory, opredelyaemye pol'zovatelem

Kol' skoro s effektivnost'yu potokov vvoda-vyvoda my uzhe razobralis', sleduet pogovorit' ob udobstve. K sozhaleniyu, dlya skol'ko-nibud' slozhnogo formatirovaniya predostavlyaemye potokami sredstva ne prednaznacheny. Ne v tom smysle, chto sredstv net, a v tom, chto oni chrezvychajno neudobny i legko vyvodyat iz sebya privykshego k elegantnomu formatu ...printf() programmista. Ne verite? Davajte poprobuem vyvesti obyknovennuyu datu v formate dd.mm.yyyy:
int day= 31,
    mon= 1,
    year=1974;

printf("%02d.%02d.%d\n", day, mon, year);  // 31.01.1974

cout<<setfill('0')<<setw(2)<<day<<'.'<<setw(2)<<mon<<setfill(' ')<<'.'
    <<year<<"\n";  // tozhe 31.01.1974
Dumayu, chto kommentarii izlishni.

Za chto zhe ne lyubyat potoki C i chem potoki C++ mogut byt' udobnee? U potokov C++ est' tol'ko odno sushchestvennoe dostoinstvo -- tipobezopasnost'. T.k. potoki C++ vse zhe nuzhno ispol'zovat', ya napisal special'nyj manipulyator, kotoryj, ostavayas' tipobezopasnym, pozvolyaet ispol'zovat' format ...printf(). On ne vyzyvaet sushchestvennyh nakladnyh rashodov i s ego pomoshch'yu privedennyj vyshe primer budet vyglyadet' sleduyushchim obrazom:

cout<<c_form(day,"02")<<'.'<<c_form(mon,"02")<<'.'<<year<<'\n';
Vot ishodnyj kod zagolovochnogo fajla:
#include <ostream>

/** lichnoe prostranstvo imen funkcii c_form, soderzhashchee detali realizacii */
namespace c_form_private {

 typedef std::ios_base::fmtflags fmtflags;
 typedef std::ostream ostream;
 typedef std::ios_base ios;

 /**
  * Vspomogatel'nyj klass dlya osushchestvleniya formatirovaniya.
  */
 class Formatter {
       /** flagi dlya ustanovki */
       fmtflags newFlags;
       /** shirina */
       int width;
       /** tochnost' */
       int prec;
       /** simvol-zapolnitel' */
       char fill;
       /** sohranyaemye flagi */
       fmtflags oldFlags;

  public:
       /**
        * Sozdaet ob容kt, ispol'zuyushchij peredannoe formatirovanie.
        */
       Formatter(const char* form, int arg1, int arg2);

       /**
        * Ustanavlivaet novoe formatirovanie dlya peredannogo potoka, sohranyaya
        * staroe.
        */
       void setFormatting(ostream& os);

       /**
        * Vosstanavlivaet pervonachal'noe formatirovanie, sohranennoe v funkcii
        * setFormatting().
        */
       void restoreFormatting(ostream& os);
 };

 /**
  * Vspomogatel'nyj klass.
  */
 template <class T>
 class Helper {
       /** vyvodimoe znachenie */
       const T& val;
       /** ob容kt dlya formatirovaniya */
       mutable Formatter fmtr;

  public:
       /**
        * Sozdaet ob容kt po peredannym parametram.
        */
       Helper(const T& val_, const char* form, int arg1, int arg2) :
         val(val_), fmtr(form, arg1, arg2) {}

       /**
        * Funkciya dlya vyvoda v potok sohranennogo znacheniya v zadannom formate.
        */
       void putTo(ostream& os) const;
 };

 template <class T>
 void Helper<T>::putTo(ostream& os) const
 {
  fmtr.setFormatting(os);
  os<<val;
  fmtr.restoreFormatting(os);
 }

 /**
  * Operator dlya vyvoda ob容ktov Helper v potok.
  */
 template <class T>
 inline ostream& operator<<(ostream& os, const Helper<T>& h)
 {
  h.putTo(os);
  return os;
 }
}

/**
 * Funkciya-manipulyator, vozvrashchayushchaya ob容kt vspomogatel'nogo klassa, dlya
 * kotorogo pereopredelen operator vyvoda v ostream. Pereopredelennyj operator
 * vyvoda osushchestvlyaet formatirovanie pri vyvode znacheniya.
 * @param val znachenie dlya vyvoda
 * @param form format vyvoda: [-|0] [chislo|*] [.(chislo|*)] [e|f|g|o|x]
 * @param arg1 neobyazatel'nyj argument, zadayushchij shirinu ili tochnost'.
 * @param arg2 neobyazatel'nyj argument, zadayushchij tochnost'.
 * @throws std::invalid_argument esli peredan argument form nekorrektnogo
 *         formata.
 */
template <class T>
inline c_form_private::Helper<T> c_form(const T& val, const char* form,
  int arg1=0, int arg2=0)
{
 return c_form_private::Helper<T>(val, form, arg1, arg2);
}
i fajla-realizacii:
#include "c_form.hpp"
#include <stdexcept>
#include <cctype>

namespace {

 /**
  * Vspomogatel'naya funkciya dlya chteniya desyatichnogo chisla.
  */
 int getval(const char*& iptr)
 {
  int ret=0;
  do ret=ret*10 + *iptr-'0';
     while (std::isdigit(*++iptr));

  return ret;
 }

}

c_form_private::Formatter::Formatter(const char* form, int arg1, int arg2) :
  newFlags(fmtflags()), width(0), prec(0), fill(0)
{
 const char* iptr=form;  // tekushchij simvol stroki formata

 if (*iptr=='-') {  // vyravnivanie vlevo
    newFlags|=ios::left;
    iptr++;
 }
 else if (*iptr=='0') {  // dobavlyaem '0'li tol'ko esli !left
         fill='0';
         iptr++;
      }

 if (*iptr=='*') {  // chitaem shirinu, esli est'
    width=arg1;
    iptr++;

    arg1=arg2;  // sdvigaem agrumenty vlevo
 }
 else if (std::isdigit(*iptr)) width=getval(iptr);

 if (*iptr=='.') {  // est' tochnost'
    if (*++iptr=='*') {
       prec=arg1;
       iptr++;
    }
    else if (std::isdigit(*iptr)) prec=getval(iptr);
         else throw std::invalid_argument("c_form");
 }

 switch (*iptr++) {
        case   0: return;  // konec stroki formata
        case 'e': newFlags|=ios::scientific; break;
        case 'f': newFlags|=ios::fixed;      break;
        case 'g':                            break;
        case 'o': newFlags|=ios::oct;        break;
        case 'x': newFlags|=ios::hex;        break;
        default: throw std::invalid_argument("c_form");
 }

 if (*iptr) throw std::invalid_argument("c_form");
}

void c_form_private::Formatter::setFormatting(ostream& os)
{
 oldFlags=os.flags();
 // ochishchaem floatfield i ustanavlivaem svoi flagi
 os.flags((oldFlags & ~ios::floatfield) | newFlags);

 if (width) os.width(width);
 if (fill)  fill=os.fill(fill);
 if (prec)  prec=os.precision(prec);
}

void c_form_private::Formatter::restoreFormatting(ostream& os)
{
 os.flags(oldFlags);

 if (fill) os.fill(fill);
 if (prec) os.precision(prec);
}
Princip ego raboty osnovan na sleduyushchej idee: funkciya c_form<>() vozvrashchaet ob容kt klassa c_form_private::Helper<>, dlya kotorogo opredelena operaciya vyvoda v ostream.

Dlya udobstva ispol'zovaniya, c_form<>() yavlyaetsya funkciej, t.k. esli by my srazu ispol'zovali konstruktor nekotorogo klassa-shablona c_form<>, to nam prishlos' by yavno zadavat' ego parametry:

cout<<c_form<int>(day,"02");
chto, myagko govorya, neudobno. Dalee. My, v principe, mogli by ne ispol'zovat' neshablonnyj klass Formatter, a pomestit' ves' kod pryamo v Helper<>, no eto privelo by k sovershenno nenuzhnoj povtornoj generacii obshchego (ne zavisyashchego ot parametrov shablona) koda.

Kak mozhno videt', realizaciyu manipulyatora c_form vryad li mozhno nazvat' trivial'noj. Tem ne menee, izuchit' ee stoit hotya by iz teh soobrazhenij, chto v processe razrabotki bylo ispol'zovano (neozhidanno) bol'shoe kolichestvo poleznyh priemov programmirovaniya.


Str.711: 21.6.2. Potoki vvoda i bufera

Funkciya readsome() yavlyaetsya operaciej nizhnego urovnya, kotoraya pozvolyaet...

T.k. privedennoe v knige opisanie readsome() tumanno, dalee sleduet perevod sootvetstvuyushchej chasti standarta:

27.6.1.3 Funkcii neformatirovannogo vvoda [lib.istream.unformatted]

streamsize readsome(char_type* s, streamsize n);
  1. Dejstviya: Esli !good() vyzyvaet setstate(failbit), kotoraya mozhet vozbudit' isklyuchenie. Inache izvlekaet simvoly i pomeshchaet ih v massiv, na pervyj element kotorogo ukazyvaet s. Esli rdbuf()->in_avail() == -1, vyzyvaet setstate(eofbit) (kotoraya mozhet vozbudit' isklyuchenie ios_base::failure (27.4.4.3)) i ne izvlekaet simvoly;
  2. Vozvrashchaet: Kolichestvo izvlechennyh simvolov.

Str.773: 23.4.3.1. |tap 1: vyyavlenie klassov

Naprimer, v matematike okruzhnost' -- eto chastnyj sluchaj ellipsa, no v bol'shinstve programm okruzhnost' ne nuzhno vyvodit' iz ellipsa, ili delat' ellips potomkom okruzhnosti.

Dumayu, chto stoit popodrobnee rassmotret' dannyj konkretnyj sluchaj, t.k. on illyustriruet dovol'no rasprostranennuyu oshibku proektirovaniya. Na pervyj vzglyad mozhet pokazat'sya, chto ideya sdelat' klass Circle proizvodnym ot klassa Ellipse yavlyaetsya vpolne priemlemoj, ved' oni svyazany otnosheniem is-a: kazhdaya okruzhnost' yavlyaetsya ellipsom. Nekorrektnost' dannoj idei stanet ochevidnoj, kak tol'ko my pristupim k napisaniyu koda.

U ellipsa, krome prochih atributov, est' dva parametra: poluosi a i b. I proizvodnaya okruzhnost' ih unasleduet. Bolee togo, nam nuzhen odin edinstvennyj radius dlya okruzhnosti i my ne mozhem dlya etih celej ispol'zovat' odin iz unasledovannyh atributov, t.k. eto izmenit ego smysl i poluchennyj ot ellipsa kod perestanet rabotat'. Sledovatel'no my vynuzhdeny dobavit' novyj atribut -- radius i, pri etom, podderzhivat' v korrektnom sostoyanii unasledovannye atributy. Ochevidno, chto podobnogo roda nasledovanie lisheno smysla, t.k. ne uproshchaet, a uslozhnyaet razrabotku.

V chem zhe delo? A delo v tom, chto ponyatie okruzhnost' v matematicheskom smysle yavlyaetsya ogranicheniem ponyatiya ellips, t.e. ego chastnym sluchaem. A nasledovanie budet polezno, esli konstruiruemyj nami ob容kt soderzhit podob容kt bazovogo klassa i vse unasledovannye operacii dlya nego imeyut smysl (rassmotrite, naprimer, operaciyu izmeneniya znacheniya poluosi b -- ona nichego ne znaet ob invariante okruzhnosti i legko ego razrushit). Drugimi slovami, ob容kt proizvodnogo klassa dolzhen byt' rasshireniem ob容kta bazovogo klassa, no ne ego chastnym sluchaem (izmeneniem), t.k. my ne mozhem povliyat' na povedenie bazovogo klassa, esli on nam ne predostavil sootvetstvuyushchih vozmozhnostej, naprimer v vide podhodyashchego nabora virtual'nyh funkcij.


Str.879: A.5. Vyrazheniya

To est' "esli nechto mozhno ponyat' kak ob座avlenie, eto i est' ob座avlenie".

T.k. slozhnye ob座avleniya C++ mogut byt' neponyatny dazhe nenovichku, stoit prokommentirovat' privedennye v knige ob座avleniya. Neochevidnost' vseh privedennyh primerov osnovana na dobavlenii lishnih skobok:

T(*e)(int(3)); ekvivalentno T* e(int(3)); To, chto inicializaciya ukazatelya s pomoshch'yu int zapreshchena, sintaksichestim analizatorom ne prinimaetsya vo vnimanie: budet raspoznano ob座avlenie ukazatelya i vydana oshibka.
T(f)[4]; ekvivalentno T f[4];
T(a); ekvivalentno T a;
T(a)=m; ekvivalentno T a=m;
T(*b)(); ob座avlenie ukazatelya na funkciyu.
T(x),y,z=7; ekvivalentno T x,y,z=7;


Str.931: B.13.2. Druz'ya

Privedennyj v konce stranicy primer nuzhno zamenit' na:
template<class C> class Basic_ops {  // bazovye operacii s kontejnerami
	friend bool operator==<>(const C&, const C&);  // sravnenie elementov
	friend bool operator!=<>(const C&, const C&);
	// ...
};
Ugolki (<>) posle imen funkcij oznachayut, chto druz'yami yavlyayutsya funkcii-shablony (pozdnie izmeneniya standarta).

|tot tekst vzyat iz spiska avtorskih ispravlenij k 10 tirazhu.

Pochemu v dannom sluchae neobhodimy <>? Potomu chto inache my ob座avlyaem drugom operator==() ne shablon, t.k. do ob座avleniya klassa v okruzhayushchem kontekste ne bylo ob座avleniya operator==()-shablona. Vot formulirovka standarta:

14.5.3. Druz'ya [temp.friend]

  1. Drugom klassa ili klassa-shablona mozhet byt' funkciya-shablon, klass-shablon, ih specializacii ili obychnaya (ne shablon) funkciya ili klass. Dlya ob座avleniya funkcij-druzej kotorye ne yavlyayutsya ob座avleniyami shablonov: Naprimer:
    template<class T> class task;
    template<class T> task<T>* preempt(task<T>*);
    
    template<class T> class task {
    	//  ...
    	friend void next_time();
    	friend void process(task<T>*);
    	friend task<T>* preempt<T>(task<T>*);
    	template<class C> friend int func(C);
    
    	friend class task<int>;
    	template<class P> friend class frd;
    	//  ...
    };
    zdes' funkciya next_time yavlyaetsya drugom kazhdoj specializacii klassa-shablona task; t.k. process ne imeet yavnyh template-arguments, kazhdaya specializaciya klassa-shablona task imeet funkciyu-druga process sootvetstvuyushchego tipa i etot drug ne yavlyaetsya specializaciej funkcii-shablona; t.k. drug preempt imeet yavnyj template-argument <T>, kazhdaya specializaciya klassa-shablona task imeet drugom sootvetstvuyushchuyu specializaciyu funkcii-shablona preempt; i, nakonec, kazhdaya specializaciya klassa-shablona task imeet drugom vse specializacii funkcii-shablona func. Analogichno, kazhdaya specializaciya klassa-shablona task imeet drugom klass-specializaciyu task<int>, i vse specializacii klassa-shablona frd.

Str.935: B.13.6. template kak kvalifikator

I snova ob etom zagadochnom kvalifikatore.

V dannom razdele d-r Straustrup privel primer ego ispol'zovaniya s funkciej-chlenom shablonom. A chto, esli nam nuzhno vyzvat' staticheskuyu funkciyu-chlen ili funkciyu-druga? Polnyj primer budet vyglyadet' sleduyushchim obrazom:

template <class T> void get_new3();  // (1)

template <class Allocator>
void f(Allocator& m)
{
 int* p1=         m.template get_new1<int>( );
 int* p2=Allocator::template get_new2<int>(m);
 int* p3=                    get_new3<int>(m);
}

struct Alloc {
       template <class T>
       T* get_new1() { return 0; }

       template <class T>
       static T* get_new2(Alloc&) { return 0; }

       template <class T>
       friend T* get_new3(Alloc&) { return 0; }
};

int main()
{
 Alloc a;
 f(a);
}
Itak:
  1. get_new1 --- eto funkciya-chlen, dlya vyzova kotoroj v dannom sluchae obyazatel'no dolzhen byt' ispol'zovan kvalifikator template. Delo v tom, chto v tochke opredeleniya f klass Allocator yavlyaetsya vsego lish' imenem parametra shablona i kompilyatoru nuzhno podskazat', chto dannyj vyzov -- eto ne (oshibochnoe) vyrazhenie (m.get_new1) < int...
  2. get_new2 -- eto staticheskaya funkciya-chlen, pri vyzove iz f, ee imya dolzhno byt' predvareno vse tem zhe kvalifikatorom template po tem zhe prichinam.
  3. A vot get_new3 -- drug klassa Alloc, privnosit v nash primer nekotorye problemy. Delo v tom, chto on ispol'zuetsya v f do ego opredeleniya v klasse Alloc (tochno tak zhe, kak ya ispol'zuyu do ih opredeleniya funkcii get_new1 i get_new2). CHtoby opredelenie f bylo korrektnym, my dolzhny garantirovat', chto imya get_new3 izvestno v tochke opredeleniya f kak imya funkcii-shablona. Daby ne ogranichivat' obshchnost' f, ya ne ispol'zoval v tochke (1) prototip konkretnoj get_new3 -- druga klassa Alloc, a prosto opisal (dazhe ne opredelil!) nekotoruyu funkciyu-shablon get_new3. Ochevidno, chto ona ne mozhet byt' ispol'zovana v f -- ona prosto delaet vyzov
    p3=get_new3<int>(m);
    legal'nym, vnosya v oblast' vidimosti nuzhnoe imya-shablon. Obratite vnimanie, chto opisannaya v tochke (1) funkciya get_new3 ne imeet parametrov i ne vozvrashchaet nikakogo znacheniya. |to sdelano dlya togo, chtoby ona ne prinimalas' vo vnimanie pri vybore podhodyashchej (vozmozhno peregruzhennoj) get_new3, v tochke ee vyzova v funkcii f.
Kak vidite, v sluchae funkcii-druga ya byl vynuzhden ispol'zovat' ne sovsem krasivyj tryuk, t.k. C++ ne pozvolyaet mne pryamo vyrazit' to, chto ya hotel, a imenno: napisat'
p3=template get_new3<int>(m);
K sozhaleniyu, prihoditsya konstatirovat', chto ispol'zovanie kvalifikatora template ne bylo v dostatochnoj mere produmano komitetom po standartizacii C++.

Optimizaciya

Pogovorim ob optimizacii.

CHto nuzhno optimizirovat'? Kogda? I nuzhno li voobshche? V etih voprosah legko zabludit'sya, esli s samogo nachala ne vybrat' pravil'nuyu tochku zreniya. Vzglyad so storony pol'zovatelya, vse srazu stavit na svoi mesta:

  1. Programma dolzhna delat' to, chto ot nee trebuetsya.
  2. Ona dolzhna eto delat' horosho.
Imenno tak: glupo optimizirovat' nepravil'no rabotayushchij kod. Esli zhe pol'zovatelya ustraivaet tekushchee bystrodejstvie -- ne stoit iskat' nepriyatnosti.

Itak, analiz proveden, reshenie prinyato -- uskoryaemsya! CHto mozhet uskorit' nashu programmu? Da vse, chto ugodno; vopros postavlen nekorrektno. CHto mozhet sushchestvenno uskorit' nashu programmu? A vot nad etim uzhe stoit podumat'.

Prezhde vsego, stoit podumat' o "vneshnem" uskorenii, t.e. o ne privodyashchih k izmeneniyu ishodnogo koda dejstviyah. Samyj shirokorasprostranennyj metod -- ispol'zovanie bolee moshchnogo "zheleza". Uvy, zachastuyu eto ne samyj effektivnyj sposob. Kak pravilo, gorazdo bol'shego mozhno dobit'sya putem pravil'nogo konfigurirovaniya togo, chto est'. Naprimer, rabota s BD -- prakticheski vsegda samoe uzkoe mesto. Dolzhno byt' ochevidno, chto pravil'naya nastrojka servera BD -- eto odno iz samyh vazhnyh dejstvij i za nego vsegda dolzhen otvechat' kompetentnyj specialist. Vy budete smeyat'sya, no grubye oploshnosti adminov proishodyat slishkom chasto, chtoby ne obrashchat' na nih vnimanie (iz moej praktiki: neodnokratno vremya raboty prilozheniya umen'shalos' s neskol'kih chasov do neskol'kih minut (!) iz-za ochevidnoj komandy UPDATE STATISTICS; fakticheski, pered analizom plana isponeniya tyazhelyh SQL-zaprosov vsegda polezno nevznachaj pointeresovat'sya aktual'nost'yu statistiki. Ne menee chastym proisshestviem yavlyaetsya "sluchajnaya poterya" indeksa vazhnoj tablicy v rezul'tate reorganizacii ili rezervnogo kopirovaniya BD).

Kol' skoro sreda ispolneniya pravil'no skonfigurirovana, stoit obratit' vnimanie neposredstvenno na kod. Ochevidno, chto maksimal'naya skorost' eskadry opredelyaetsya skorost'yu samogo medlennogo korablya. On-to nam i nuzhen. Esli "eskadroj" yavlyaetsya nabor SQL-zaprosov rabotayushchego s BD prilozheniya, to, kak pravilo, nikakih trudnostej s opredeleniem uzkih mest ne voznikaet. Trudnosti voznikayut s opredeleniem uzkih mest "obychnyh" prilozhenij.

Uzkie mesta nuzhno iskat' tol'ko s pomoshch'yu ob容ktivnyh izmerenij, t.k. intuiciya v dannoj oblasti chashche vsego ne srabatyvaet (ne stoit utverzhdat', chto ne rabotaet voobshche). Prichem izmeryat' otnositel'nuyu proizvoditel'nost' imeet smysl tol'ko pri "reliz"-nastrojkah kompilyatora (pri otklyuchennoj optimizacii uzkie mesta mogut byt' najdeny tam, gde ih net. Uvy, dannogo roda oshibki dopuskayut dazhe opytnye programmisty) i na real'nyh "vhodnyh dannyh" (tak, naprimer, otlichnye sravnitel'nye harakteristiki v sortirovke ravnomerno raspredelennyh int, otnyut' ne garantiruyut otlichnuyu rabotu na real'nyh klyuchah real'nyh dannyh). Dejstvitel'no ser'eznym podspor'em v poiske uzkih mest yavlyayutsya profajlery -- neot容mlemaya chast' lyuboj professional'noj sredy razrabotki.

Kogda kriticheskij uchastok koda lokalizovan, mozhno pristupat' k neposredstvennomu analizu. S chego nachat'? Nachinat' nuzhno s samyh resursoemkih operacij. Kak pravilo, po trebuemomu dlya ispolneniya vremeni, operacii legko razdelyayutsya na sloi, otlichayushchiesya drug ot druga na neskol'ko poryadkov:

  1. rabota s vneshnimi ustrojstvami
  2. sistemnye vyzovy
  3. vyzovy sobstvennyh funkcij
  4. lokal'nye upravlyayushchie struktury
  5. special'nyj podbor komand i optimal'noe ispol'zovanie registrov
Naprimer, ne stoit zanimat'sya voprosami razmeshcheniya upravlyayushchej peremennoj cikla v sootvetstvuyushchem registre processora, esli v dannom cikle proishodit obrashchenie k disku. Vyzovy sobstvennyh funkcij sushchestvenno otlichayutsya ot sistemnyh vyzovov tem, chto kogda my obrashchaemsya k sisteme, proishodit pereklyuchenie konteksta potoka (sistemnyj kod imeet bol'she privilegij, obrashchat'sya k nemu mozhno tol'ko cherez special'nye shlyuzy) i obyazatel'naya proverka dostovernosti peredannyh argumentov (naprimer, sistema proveryaet dejstvitel'no li ej peredana korrektnaya stroka putem ee posimvol'nogo skanirovaniya; esli pri etom proizojdet narushenie prav dostupa ili oshibka adresacii, to prilozhenie budet ob etom proinformirovano; tem samym isklyuchaetsya vozmozhnost' sboya vnutri yadra sistemy, kogda neyasno chto delat' i kto vinovat; naibolee veroyatnyj rezul'tat -- blue death screen, system trap i t.d., t.e. nevosstanovimyj sboj samoj sistemy).

Kak pravilo, tol'ko v isklyuchitel'nyh sluchayah zametnogo uskoreniya raboty mozhno dostich' putem lokal'nyh uluchshenij (kotorymi pestryat drevnie nastavleniya: a+a vmesto 2*a, register int i; i t.d.), sovremennye kompilyatory prekrasno spravlyayutsya s nimi bez nas (vmeste s tem, generaciya kompilyatorom nedostatochno optimal'nogo koda "v dannom konkretnom meste" vse eshche ne yavlyaetsya redkost'yu). Ser'eznye uluchsheniya obychno prinosit tol'ko izmenenie algoritma raboty.

Pervym delom stoit obratit' vnimanie na sam algoritm (klassicheskim primerom yavlyaetsya sortirovka s algoritmami O(N*N), O(N*log(N)) i O(N*M) stoimosti ili vybor podhodyashchego kontejnera). No ne popadite v lovushku! Pamyat' sovremennyh komp'yuterov uzhe ne yavlyaetsya ustrojstvom proizvol'nogo dostupa, v tom smysle, chto promah mimo kesha pri nevinnom obrashchenii po ukazatelyu mozhet obojtis' gorazdo dorozhe vyzova trivial'noj funkcii, chej kod uzhe popal v kesh. Izvestny sluchai, kogda izmenenie prohoda bol'shoj dvumernoj matricy s posledovatel'nogo postrochnogo na "obmanyvayushchij" kesh postolbcovyj zamedlyalo rabotu algoritma v neskol'ko raz!

Esli zhe principial'nyj algoritm iznachal'no optimalen, mozhno poprobovat' ispol'zovat' zamenu urovnej resursoemkosti. Klassicheskim primerom yavlyaetsya vse to zhe keshirovanie. Naprimer vmesto dorogostoyashchego schityvaniya dannyh s diska, proishodit obrashchenie k zaranee podgotovlennoj kopii v pamyati, tem samym my perehodim s pervogo urovnya na vtoroj-tretij. Stoit otmetit', chto tehnika keshirovaniya nahodit svoe primenenie ne tol'ko v rabote s vneshnimi ustrojstvami. Esli, naprimer, v igrovoj programme uzkim mestom stanovitsya vychislenie sin(x), to stoit podumat' ob ispol'zovanii zaranee rasschitannoj tablicy sinusov (obychno dostatochno 360 znachenij tipa int vmesto potencial'no bolee dorogoj plavayuchej arifmetiki). Bolee "prikladnoj" primer -- eto dlinnyj switch po tipam soobshchenij v ih obrabotchike. Esli on stal uzkim mestom, podumajte ob ispol'zovanii tablicy perehodov ili heshirovaniya (stoimost' O(1)) ili zhe special'noj drevovidnoj struktury (stoimost' O(log(N))) -- sushchestvenno luchshe O(N), obychno obespechivaemogo switch. Nu a pro vozmozhnost' ispol'zovaniya virtual'noj funkcii vmesto switch ya dazhe ne stanu napominat'.

Vse eti zamechaniya primenimy v ravnoj stepeni k lyubomu yazyku. Davajte posmotrim na chto stoit obratit' vnimanie programmistam na C++.

Prezhde vsego, stoit otmetit', chto vse bolee-menee sushchestvennye malen'kie hitrosti sobstvenno C++ uzhe byli rassmotreny v predydushchih primerah, tak zhe kak i skrytye nakladnye rashody. Byt' mozhet, za kadrom ostalas' tol'ko vozmozhnost' "oblegchennogo vyzova funkcii", t.k. ona yavlyaetsya ne chast'yu (standartnogo) C++, a osobennost'yu konkretnyh realizacij.

C++ kak i C pri vyzove funkcii razmeshchaet parametry v steke. T.e. imeya parametr v registre, kompilyator zanosit ego v stek, vyzyvaet funkciyu, a v tele funkcii opyat' perenosit parametr v registr. Vsego etogo mozhno izbezhat' ispol'zovav sootvetstvuyushchee soglashenie vyzova (v nekotoryh realizaciyah ispol'zuetsya zarezervirovannoe slovo _fastcall), kogda parametry pered vyzovom razmeshchayutsya neposredstvenno v registrah, isklyuchaya tem samym nenuzhnye stekovye operacii. Naprimer v prostom teste:

void f1(int arg)
{
 Var+=arg;
}

void _fastcall f2(int arg)
{
 Var+=arg;
}
funkciya f1() rabotala na 50% medlennee. Konechno, real'nuyu vygodu iz etogo fakta mozhno poluchit' tol'ko pri massovom ispol'zovanii funkcij oblegchennogo vyzova vo vsem proekte. I eta sovershenno besplatnaya raznica mozhet byt' dostatochno sushchestvennoj.

Eshche odin nemalovazhnyj faktor -- razmer programm. Otkuda vzyalis' vse eti sovremennye megabajty? Uvy, bol'shaya ih chast' -- mertvyj kod, real'no, bolee 90% zagruzhennogo koda nikogda ne budet vyzvano! Ne beda, esli eti megabajty prosto lezhat na diske, real'nye trudnosti poyavlyayutsya, kogda vy zagruzhaete na vypolnenie neskol'ko takih monstrov. Padenie proizvoditel'nosti sistemy vo vremya vydeleniya dopolnitel'noj virtual'noj pamyati mozhet stat' prosto katastroficheskim.

Esli pri razrabotke bol'shogo proekta iznachal'no ne priderzhivat'sya politiki strogogo opredeleniya zavisimostej mezhdu ishodnymi fajlami (i ne prinimat' ser'eznyh mer dlya ih minimizacii), to v itoge, dlya uspeshnoj linkovki budet neobhodimo podklyuchit' slishkom mnogo musora iz standartnogo instrumentariya dannogo proekta. V neskol'ko raz bol'she, chem poleznogo koda. Iz-za chego eto proishodit? Esli funkciya f() iz file1.cpp vyzyvaet g() iz file2.cpp, to, ochevidno, my obyazany podklyuchit' file2.cpp k nashemu proektu. Pri etom, esli ne bylo prinyato special'nyh mer, to v file2.cpp pochti vsegda najdetsya kakaya-nibud' g2(), kak pravilo ne nuzhnaya dlya raboty g() i vyzyvayushchaya funkcii eshche kakogo-libo fajla; i poshlo-poehalo... A kogda kazhdoe prilozhenie soderzhit svyshe polusotni ishodnyh fajlov, a v proekte neskol'ko soten prilozhenij, to navesti poryadok postfaktum uzhe ne predstavlyaetsya vozmozhnym.

Otlichnoe obsuzhdenie lokal'nyh priemov optimizacii mozhno najti u Paul Hsieh "Programming Optimization". Ne ochen' glubokij, a mestami i otkrovenno "slabyj", no, tem ne menee, prakticheski poleznyj obzor bolee vysokourovnevyh tehnik predstavlen v knige Steve Heller "Optimizing C++".


Makrosy

V C++ makrosy ne nuzhny! Do boli znakomoe vyskazyvanie, ne tak li? YA by ego nemnogo utochnil: ne nuzhny, esli vy ne hotite sushchestvenno oblegchit' sebe zhizn'.

YA polnost'yu soglasen s tem, chto chrezmernoe i neobdumannoe ispol'zovanie makrosov mozhet vyzvat' bol'shie nepriyatnosti, osobenno pri povtornom ispol'zovanii koda. Vmeste s tem, ya ne znayu ni odnogo sredstva C++, kotoroe moglo by prinesti pol'zu pri chrezmernom i neobdumannom ego ispol'zovanii.

Itak, kogda makrosy mogut prinesti pol'zu?

  1. Makros kak nad座azykovoe sredstvo. Horoshij primerom yavlyaetsya prostoj, no udivitel'no poleznyj otladochnyj makros _VAL_, vyvodyashchij imya i znachenie peremennoj:
    #define _VAL_(var) #var "=" << var << " "
    Nad座azykovoj chast'yu zdes' yavlyaetsya rabota s peremennoj kak s tekstom, putem perevoda imeni peremennoj (ono sushchestvuet tol'ko v ishodnom kode programmy) v strokovyj literal, real'no sushchestvuyushchij v kode binarnom. Dannuyu vozmozhnost' mogut predostavit' tol'ko makrosy.
  2. Informaciya o tekushchem ishodnom fajle i stroke -- ee pol'zu pri otladke trudno pereocenit'. Dlya etogo ya ispol'zuyu special'nyj makros _ADD_. Naprimer:
    	cout<<_ADD_("Oshibka chteniya");
    vyvedet chto-to vrode
    Oshibka chteniya <file.cpp:34>
    A esli nuzhen perevod stroki, to stoit poprobovat'
    	cout<<"Oshibka chteniya" _ADD_("") "\n";
    Takoj metod rabotaet, potomu chto makros _ADD_ vozvrashchaet strokovyj literal. Vrode by ekvivalentnaya funkciya
    	char* _ADD_(char*);
    vpolne podoshla by dlya pervogo primera, no ne dlya vtorogo. Konechno, dlya vyvoda v cout eto ne imeet nikakogo znacheniya, no v sleduyushchem punkte ya pokazhu principial'nuyu vazhnost' podobnogo povedeniya.

    Rassmotrim ustrojstvo _ADD_:

    #define _ADD_tmp_tmp_(str,arg) str " <" __FILE__ ":" #arg ">"
    #define _ADD_tmp_(str,arg) _ADD_tmp_tmp_(str,arg)
    #define _ADD_(str) _ADD_tmp_(str,__LINE__)
    Pochemu vse tak slozhno? Delo v tom, chto __LINE__ v otlichie ot __FILE__ yavlyaetsya chislovym, a ne strokovym literalom i chtoby privesti ego k nuzhnomu tipu pridetsya proyavit' nekotoruyu smekalku. My, konechno, ne mozhem napisat':
    #define _ADD_(str) str " <" __FILE__ ":" #__LINE__ ">"
    t.k. # mozhet byt' primenen tol'ko k argumentu makrosa. Resheniem yavlyaetsya peredacha __LINE__ v vide parametra nekotoromu vspomogatel'nomu makrosu, no ochevidnoe
    #define _ADD_tmp_(str,arg) str " <" __FILE__ ":" #arg ">"
    #define _ADD_(str) _ADD_tmp_(str,__LINE__)
    ne rabotaet: rezul'tatom _ADD_("Oshibka chteniya") budet
    "Oshibka chteniya <file.cpp:__LINE__>"
    chto netrudno bylo predvidet'. V itoge my prihodim k privedennomu vyshe variantu, kotoryj obrabatyvaetsya preprocessorom sleduyushchim obrazom: _ADD_("Oshibka chteniya") posledovatel'no podstavlyaetsya v
    _ADD_tmp_("Oshibka chteniya",__LINE__)
    _ADD_tmp_tmp_("Oshibka chteniya",34)
    "Oshibka chteniya" " <" "file.cpp" ":" "34" ">"
    "Oshibka chteniya <file.cpp:34>"
  3. Poluchenie znacheniya chislovogo makrosa v vide stroki. Kak pokazyvaet praktika, dannaya vozmozhnost' nahodit sebe primenenie i za predelami podrobnostej realizacii "mnogoetazhnyh" makrosov. Dopustim, chto dlya vzaimodejstviya s SQL-serverom u nas opredelen klass DB::Query s sootvetstvuyushchej funkciej
    void DB::Query::Statement(const char *);
    i my hotim vybrat' vse stroki nekotoroj tablicy, imeyushchie ravnoe nekomu "magicheskomu chislu" pole somefield:
    #define FieldOK 7
    // ...
    DB::Int tmp(FieldOK);
    q.Statement(" SELECT * "
                " FROM sometable "
                " WHERE somefield=? "
    );
    q.SetParam(), tmp;
    Izlishne mnogoslovno. Kak by eto nam ispol'zovat' FieldOK napryamuyu? Nedostatochno znakomye s vozmozhnostyami makrosov programmisty delayut eto tak:
    #define FieldOK 7
    // ...
    #define FieldOK_CHAR "7"
    // ...
    q.Statement(" SELECT * "
                " FROM sometable "
                " WHERE somefield=" FieldOK_CHAR
    );
    V rezul'tate chego vy poluchaete vse prelesti sinhronizacii izmenenij vzaimosvyazannyh naborov makrosov so vsemi vytekayushchimi iz etogo oshibkami. Pravil'nym resheniem budet
    #define FieldOK 7
    // ...
    q.Statement(" SELECT * "
                " FROM sometable "
                " WHERE somefield=" _GETSTR_(FieldOK)
    );
    gde _GETSTR_ opredelen sleduyushchim obrazom:
    #define _GETSTR_(arg) #arg
    Kstati, privedennyj primer naglyadno demonstriruet nevozmozhnost' polnost'yu ekvivalentnoj zameny vseh chislovyh makrosov na prinyatye v C++
    const int FieldOK=7;
    enum { FieldOK=7 };
    makros _GETSTR_ ne smozhet s nimi rabotat'.
  4. Mnogokratno vstrechayushchiesya chasti koda. Rassmotrim eshche odin primer iz oblasti raboty s SQL-serverom. Predpolozhim, chto nam nuzhno vybrat' dannye iz nekotoroj tablicy. |to mozhno sdelat' v lob:
    struct Table1 {  // predstavlenie dannyh tablicy
           DB::Date  Field1;
           DB::Int   Field2;
           DB::Short Field3;
    };
    
    void f()
    {
     Table1 tbl;
     DB::Query q;
     q.Statement(" SELECT Field1, Field2, Field3 "
                 " FROM Table1 "
     );
     q.BindCol(), tbl.Field1, tbl.Field2, tbl.Field3;
     // ...
    }
    I etot metod dejstvitel'no rabotaet. No chto, esli predstavlenie tablicy izmenilos'? Teper' nam pridetsya iskat' i ispravlyat' vse podobnye mesta -- chrezvychajno utomitel'nyj process! Ob etom stoilo pozabotit'sya zaranee:
    #define TABLE1_FLD      Field1, Field2, Field3
    #define TABLE1_FLD_CHAR "Field1, Field2, Field3"
    
    struct Table1 {  // predstavlenie dannyh tablicy
           DB::Date  Field1;
           DB::Int   Field2;
           DB::Short Field3;
    
           // vspomogatel'naya funkciya
           void BindCol(DB::Query& q) { q.BindCol(), TABLE1_FLD; }
    };
    
    void f()
    {
     Table1 tbl;
     DB::Query q;
     q.Statement(" SELECT " TABLE1_FLD_CHAR
                 " FROM Table1 "
     );
     tbl.BindCol(q);
     // ...
    }
    Teper' izmenenie struktury tablicy obojdetsya bez zubovnogo skrezheta. Stoit otmetit', chto v opredelenii TABLE1_FLD_CHAR ya ne mog ispol'zovat' ochevidnoe _GETSTR_(TABLE1_FLD), t.k. TABLE1_FLD soderzhit zapyatye. K sozhaleniyu, dannoe pechal'noe ogranichenie v primitivnom preprocessore C++ nikak nel'zya obojti.
  5. Mnogokratno vstrechayushchiesya podobnye chasti koda. Predstavim sebe, chto my pishem prilozhenie dlya bankovskoj sfery i dolzhny vybrat' informaciyu po nekotorym schetam. V Rossii, naprimer, schet sostoit iz mnogih polej, kotorye dlya udobstva raboty sobirayut v special'nuyu strukturu, a v tablice on mozhet byt' predstavlen smezhnymi polyami s odinakovym prefiksom:
    q.Statement(" SELECT Field1, AccA_bal, AccA_cur, AccA_key, AccA_brn, "
                " AccA_per, Field2 "
                " FROM Table1 "
    );
    q.BindCol(), tbl.Field1, tbl.AccA.bal, tbl.AccA.cur, tbl.AccA.key,
                 tbl.AccA.brn, tbl.AccA.per, tbl.Field2;
    // ...
    Mozhete sebe predstavit', skol'ko pisaniny trebuetsya dlya vybora chetyreh schetov (tbl.AccA, tbl.AccB, tbl.KorA, tbl.KorB). I snova na pomoshch' prihodyat makrosy:
    #define _SACC_(arg) #arg"_bal, "#arg"_cur, "#arg"_key, "#arg"_brn, " \
                        #arg"_per "
    #define _BACC_(arg) arg.bal, arg.cur, arg.key, arg.brn, arg.per
    
    // ...
    
    q.Statement(" SELECT Field1, " _SACC_(AccA) " , Field2 "
                " FROM Table1 "
    );
    q.BindCol(), tbl.Field1, _BACC_(tbl.AccA), tbl.Field2;
    // ...
    Dumayu, chto kommentarii izlishni.
  6. Rassmotrim bolee tonkij primer podobiya. Pust' nam potrebovalos' sozdat' tablicu dlya hraneniya chasto ispol'zuemoj nami struktury dannyh:
    struct A {
           MyDate Date;
           int    Field2;
           short  Field3;
    };
    My ne mozhem ispol'zovat' identifikator Date dlya imeni stolbca tablicy, t.k. DATE yavlyaetsya zarezervirovannym slovom SQL. |ta problema legko obhoditsya s pomoshch'yu pripisyvaniya nekotorogo prefiksa:
    struct TableA {
           DB::Date  xDate;
           DB::Int   xField2;
           DB::Short xField3;
    
           TableA& operator=(A&);
           void Clear();
    };
    A teper' opredelim funkcii-chleny:
    TableA& TableA::operator=(A& a)
    {
     xDate=ToDB(a.Date);
     xField2=ToDB(a.Field2);
     xField3=ToDB(a.Field3);
    
     return *this;
    }
    
    void TableA::Clear()
    {
     xDate="";
     xField2="";
     xField3="";
    }
    Garantiruyu, chto esli TableA soderzhit hotya by paru-trojku desyatkov polej, to napisanie podobnogo koda vam ochen' bystro naskuchit, myagko govorya! Nel'zya li eto sdelat' odin raz, a potom ispol'zovat' rezul'taty? Okazyvaetsya mozhno:
    TableA& TableA::operator=(A& a)
    {
    // ispol'zuem sklejku leksem: ##
    #define ASS(arg) x##arg=ToDB(a.arg);
     ASS(Date);
     ASS(Field2);
     ASS(Field3);
    #undef ASS
    
     return *this;
    }
    
    void TableA::Clear()
    {
    #define CLR(arg) x##arg=""
     CLR(Date);
     CLR(Field2);
     CLR(Field3);
    #undef CLR
    }
    Teper' opredelenie TableA::Clear()po TableA::operator=() ne neset nikakoj nudnoj raboty, esli, konechno, vash tekstovyj redaktor podderzhivaet komandy poiska i zameny. Tak zhe prosto mozhno opredelit' i obratnoe prisvaivanie: A& A::operator=(TableA&).
Nadeyus', chto posle privedennyh vyshe primerov vy po-novomu posmotrite na rol' makrosov v C++.
Copyright © S. Derevyago, 2000-2004

Nikakaya chast' dannogo materiala ne mozhet byt' ispol'zovana v kommercheskih celyah bez pis'mennogo razresheniya avtora.