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.
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.1974Dumayu, 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.
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);
!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;
rdbuf()->in_avail() == 0
, ne izvlekaet simvoly
rdbuf()->in_avail() > 0
, izvlekaet min(rdbuf()->in_avail(),n))
simvolov
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.
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; |
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]
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
.
template
kak kvalifikatorV 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:
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...
get_new2
-- eto staticheskaya funkciya-chlen, pri vyzove iz f
, ee imya dolzhno byt' predvareno vse tem zhe kvalifikatorom template
po tem zhe prichinam.
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
.
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++.
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:
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:
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++".
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?
_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.
_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>"
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) #argKstati, 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'.
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.
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.
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&)
.
Nikakaya chast' dannogo materiala ne mozhet byt' ispol'zovana v kommercheskih celyah bez pis'mennogo razresheniya avtora.