Що е програмиране?

      Широко разпространено заблуждение е, че "програмиране" и "писане на програма" са две еквиваленти неща. Това твърдение е широко разпространено поради простотата на запомняне и лошата осведоменост на хората които не боравят с електроника и компютри (всъщност и на по-голямата част от тези които боравят). А е заблуждение, защото не е вярно.
      Точна дефиниция на това понятие всъщност няма. Всеки опит да се даде кратко определение на "програмиране" води или до нещо толкова неясно и неразбрано, че само по себе си няма смисъл или до разтегнат текст които дава смисъл в общ случай и след това оборва сам себе си с всички частни случай за които се е сетил авторът. И в двата случая накрая се стига до смъртоносно напиване. Поради тази причина аз няма да се опитвам да дам определение, а ще дам една примерна схема по която се прави добре работеща програма:
  1. Запознаване със задачата която програмата трябва да разреши
  2. Запознаване с аналитичните и практическите методи по които задачата се решава по принцип (имам предвид ВСИЧКИ методи)
  3. Сравняват се резултатите давани от различните методи, и тези които не дават задоволителни резултати се отстраняват. Избира се един или няколко метода които програмата ще използва
  4. Алгоритмизиране на избраните методи
  5. Измисляне на алгоритъм на програмата
  6. Анализиране на избрания алгоритъм, и ръчно решаване на задачата по избрания алгоритъм с няколко различни начални параметри. Ако алгоритъма не решава задачата той или се модифицира или се избира друг алгоритъм.
  7. Построяване на интерфейс на програмата (и дизайн на външния вид)
  8. Начертаване работна блок-схема на програмата
  9. Написване сорс кода. В този стадий обикновено се налагат модификации на части от основния алгоритъм на програмата.
  10. Тестване на програмата и модификация на сорс кода
  11. Документиране на програмата и бета тестове
      Както се вижда, написването на програмата (сорс кода) е само етап от самото програмиране и то далеч не най-съществения. На практика, написването на сорс кода е не повече от 10% от цялата работа, докато работата по алгоритмизирането на методите, и създаване на същинския алгоритъм заема около 80%.
      Или с едно изречение разбиваме заблудата: "Програмиране" е комплексна съвкупност от операции, и написването на програмата е само малка част от тази съвкупност.


Идеология на програмирането

      Както вече казахме, написването на програмата не е най-основната част от проекта. Това обаче съвсем не означава, че самия сорс код е нещо маловажно, тъй като кода на програмата, написан на какъвто и да е език е израз на алгоритъма заложен вътре. От това е лесно да се стигне до извода, че един съвършен алгоритъм написан калпаво, ще бъде точно толкова ефективен колкото и един калпав алгоритъм, оптимизиран перфектно. Та като казах оптимизиран, се сетих че трябва да обясня какво значи това.
      Оптимизация на кода, означава промяна на кода с цел печалба на машинно време (оптимизация по бързодействие) или на памет (оптимизация по краткост). Оптимизацията се извършва още по време на написването на кода, и/или след написване на цялата програма.
      Двата вида оптимизиране не са точно взаимоизключващи се, но много рядко може да се приложат върху една и съща програма. Трябва да отбележим, че тук не се има предвид оптимизацията на самия алгоритъм, а оптимизация на неговата реализация.
      Други изисквания към кода са четливостта и модулността. Тъй като обаче ще се спрем на всяко едно изискване, сега няма да го обяснявам.

Бързодействие

      За да не стане недоразумение, още отначало искам да подчертая: Под бързодействие в програмирането се разбира бързината с която една програма си върши работата, а не колко бързо тя е написана!
      Един от основните критерии за производителността на един компютър е тактовата честота (бързината) на неговия процесор. Тактовата честота е с дименсия Херц (Hz) и същността й означава тактовете които прави процесора за една секунда. Поради множеството различни честоти в един компютър, за удобство тактовата честота на процесора наричаме тик (tick).
      Всяка една програма представлява множество от машинни инструкции които се изпълняват от процесора една слуд друга (теоретично), като обработването на една инструкция отнема от един до дванайсет или повече тика. Например, ако приемем че всяка инструкция се изпълнява средно за 6 тика, то един процесор с тактова честота 1.2 GHz или 1 200 000 000 Hz, ще изпълнява по 200 000 000 машинни инструкции за една секунда. В по-старите процесори се знаеше колко точно тика отнема всяка една инструкция, но от Pentium ядрото на процесора претърпя основни промени, и в по-новите процесори инструкциите отнемат различен брой тикове в зависимост от контекста на програмата.
      След казаното до тук, вече може да обясним основните начини за оптимизиране по бързина:
В системното програмиране:

Съкращаване на инструкциите
   Ясно е, че ако съкратим броя на инструкциите на програмата, то ще намалим и времето за нейното изпълнение, а също и големината на кода. На практика обаче, в един добре написан код няма инструкции които да бъдат премахнати, така че този метод се прилага само след първоначалното написване на програмата, за да се премахнат всички ненужни инструкции.

Разместване на инструкциите
   В новите процесори, чрез подреждането на едни и същи инструкции по различни начини, се постига доста голяма разлика в бързодействието. Прилагането на този метод обаче е приложим само при добро познаване на конкретния процесор. Разбира се, трябва да се внимава при разместването на инструкциите да не се промени смисъла на кода.

Условна промяна на инструкциите
   Условна промяна на инструкциите, означава да се употребят други инструкции за едно и също действие, без да променяме същността на самото действие. Например:
Вариант I   Вариант II
LODSDMOV EAX, dword ptr DS:[ESI]
ADD ESI, 4
   Двата варианта извършват едни и същи действия - зареждане на 32-битова стойност от адреса посочен от DS:[ESI] в регистъра EAX, и увеличаване стойността на ESI с 4. Ясно е, че първият вариант е много по-кратък, но за сметка на това вторият е много по-гъвкав, тъй като двете инструкции (MOV и ADD) можем да разположим в контекста така, че да ускорим изпълнението не само на това действие.

Реална промяна на инструкциите
   Прилагането на този метод означава да се промени действието, но да не се промени резултата от него. Например:
Вариант I   Вариант II
ADD AX, 2INC AX
INC AX
   В този пример е използвана еквивалентността на действието "събиране с две" (Вариант I) с два пъти приложеното действие "увеличаване с единица" (Вариант II). Наистина, от гледна точка на математиката, няма никакво значение дали ще прибавим към една променлива веднъж две, или два пъти едно. Но от програмна гледна точка, разликата е съществена, тъй като инструкцията ADD отнема 3-5 и повече тика, докато инструкцията INC - само един. Така че, втория вариант е хем по-кратък (инструкцията INC е с дължина един байт, а ADD е 5-6), хем по-бързо се изпълнява. Не винаги обаче той може да се приложи.
   Когато се извършва промяна на инструкциите, трябва да се имат предвид следните съотношения на скоростите:
  • Логически и основни аритметични инструкции (най-бързи) - OR, AND, XOR, INC, DEC, SHL, SHR и др.
  • Прости аритметични инструкции - ADD, SUB и др.
  • Сложни аритметични инструкции (най-бавни) - MUL, DIV и др.
   Също могат да се вземат предвид и някои особености на бинарната математика. В следващия пример, в регистъра AX се зарежда числото 14, и от него се изважда числото 5:
Вариант I   Вариант II
MOV AX, 14MOV AX, 14
SUB AX, 5ADD AX, -5
   Тук е трудно да се каже дали променями самото действие, тъй като от математическа гледна точка няма значение дали ще извадим положително число или ще прибавим отрицателно. Но в бинарната алгебра, операцията "изваждане" се свежда до операция "събиране", като умалителя предварително се преработва. От тук следва, че вторият вариант е по-бърз с един-два тика. Дали в случая промяната на инструкциите е реална или условна е въпрос който може да се обсъди от цяла орда философи, но всъщност нас това не ни грее - важното е, че печелим скорост.
В приложното програмиране:

Реална промяна на инструкциите
   В приложното програмиране само този метод може да бъде реализиран като контрол върху самите машинни инструкции, тъй като реалният машинен код се получава едва след компилирането на сорса. Същността на метода е същия - заместване на едно действие с друго, което дава същия резултат. Например, за получаване адреса на пиксел с координати X,Y в режим 320х200, се използва формулата:
PixelAddress = BufferAddress+Y*320+X;
   Тази формула може да се преработи, като вместо Y*320 използваме (Y<<8)+(Y<<6), което действие дава еквивалентен резултат, защото:
Y*320 = Y*(256+64) = (Y*256)+(Y*64) = (Y<<8)+(Y<<6)
   Преработената формула става:
PixelAddress = BufferAddress+(Y<<8)+(Y<<6)+X;
   Печалбата от това е, че заместваме сравнително бавната операция "умножение" с далеч по-бързите операции "изместване" и "събиране". Макар да се използват три операции вместо една, печалбата на скорост е гарантирана.

Обединяване на цикли
   Идеята е проста: Ако в един масив (или друга база данни) се правят една след друга две операции в цикъл, вместо да се правят два (или повече) цикъла един след друг, се стремим да обединим операциитев един единствен. Този метод се използва и в системното програмиране, но там той е много изменен, и същността му се основава на друга идея.

Съкращаване на изчисления
   Това не е точно метод, но идеята е също проста. Ако в една функция на две или повече места се прави едно и също изчисление, е по-добре то да се направи веднъж и да се запише в някаква променлива, и навсякъде където е необходим резултатът от изчислението, той се извлича от въпросната променлива.
      Разбира се има и много други методи, но тук се опитваме само да дадем някаква представа за работата на програмиста когато оптимизира една програма.

Краткост

      Краткостта на програмата е относително понятие. И това е съвсем естетвено - не може да се сравнява по големина един текстов редактор с една стратегическа игра. Т.е. оценката за краткост се дава в зависимост от предназначението на програмата и работата която тя в действителност върши.
      Вече дадохме някаква идея как се постига краткост в системното програмиране и казахме, че краткостта до някъде води и до бързодействие, но трябва да изясним защо всъщност е нужно една програма да е кратка. Ами, за да не заема много памет. Преди години, оперативната памет (RAM паметта) на компютъра е била твърде скъпа за да се изразходва неразумно. Затова голяма част от програмите (включително и операционните системи) са оптимизирани по краткост. В днешно време RAM-а е достатъчно евтин и малко хора се замислят над това до колко оптимизирана е по краткост тяхната програма. Не мислете, че това създава особен проблем - оптимизирането на една програма по краткост, води до съкращения от порядъка на байтове, което сравнено с цената на паметта е на практика пренебрежимо малко.
      Но разхищението на памет все пак е ограничено, по един доста елегантен според мен начин - това са динамичните библиотеки (DLL), но колко е елегантна реализацията на този начин е друг въпрос. Идеята е, че различни програми използват едни и същи функции, които вместо да се имплементират в кода на всяка отделна програма се компилират в отделна библиотека. Когато една програма се нуждае от дадена функция от DLL, тя я изисква от операционната система, която от своя страна свързва съответната DLL и програмата. По този начин се пести памет от доста по-висок порядък - хвърлете един поглед колко са големи DLL файловете (на моята система заемат 133MB), и умножете размера по броя на програмите които имате инсталирани. Разбира се, една програма не използва всички динамични библиотеки, но ще получите добра представа за порядъка на икономия. Това обаче е за сметка на бързодействие.

Подреждане

      Подреждането е нещо към което всички програмисти се отнасят с особен респект - стоят на прилично разстояние от него. Това е и една от причините около тях да цари висока ентропия и да се радват на удобството всичко което им потрябва да е пред очите им... Е, някъде пред очите им.
      Има обаче две специфични значения на тази дума които са изключения. Първото и по-често употребявано от тях, е подредеността на сорса. Това има смисъл на четливост и е разгледано отделно.
      Второто значение на "подреждане" има проблем от чисто лингвистичен характер. Подреждане на български език е преведено не съвсем правилно от английското alignment, което буквално означава изравняване или подравняване. Въпросното подравняване се появява при някои елементи на системното програмиране, при които се цели висока скорост на изпълнение. То се състои в подреждане на командите и променливите по отмествания кратни на определена степен на две, при което се губят байтове от адресното пространство. Звучи по-сложно от колкото е в действителност. Идеята се основава на начина на работа на компютъра при четене от/запис в паметта, и работата на кеша.
      Компютъра е програмиран така, че да използва максимално възможностите на хардуера - например една 64-битова външна шина пренася винаги 64-бита. Нито повече, нито по-малко. Затова ако ние имаме записани някъде в паметта 16-битова променлива, какво трябва да имаме предвид? Тези 64-бита които външната шина предава се вземат от адрес кратен на 8 (байта). Ако нашата променлива се намира на адрес с отместване 31 (заема байтове 31 и 32) извличането й ще заеме два пъти повече време, отколкото ако тя е на адреси с отместване между 24 и 30. Първо ще се извлекат 64-бита от отместване 24 и ще се запишат в кеша (байт 31 е последните 8 бита), след тях ще се извлекат 64-бита от отместване 32 и ще се добавят след първите (байт 32 е първите 8 бита). След това 16-те бита ще се изпратят по вътрешната шина за обработка. Но ако променливата е например на адрес с отместване 27, то цялата променлива ще бъде получена с един транш, а не с два.
      За да бъдат подредени данните (променливи или команди) както трябва, на определени места са поставя командата "ALIGN value" която указва да се използва отместване кратно на value. Например - "ALIGN 8", "ALIGN 16" и т.н. Ясно е, че клетките от паметта намиращи се непосредствено преди командата ще останат неизползвани, но пък за сметка на това имаме бързодействие.
      Ако все пак не разбирате това което прочетохте току-що, пийте една Бира и да не ви пука. Както казах, този вид "подреждане" се явява при някои елементи на системното програмиране, които не се срещат често. Това са драйвери, системни функции, елементи на операционна система и др. от които се изисква изключително висока скорост и ако някога ви се наложи да пишете такива неща, ще ги разберете от самосебеси. Тук само искаме да дадем представа за идеологията на програмирането или какво става в главата на един програмист.
      Преди да продължим нататък из дебрите на програмирането искам само да вметна още нещо относно лингвистичната особеност на въпросното "подреждане". Както казах, превода на alignment не е съвсем точен и в работна обстановка се използват различни думи от програмисткия жаргон за това действие. Както всеки жаргон, думите трябва директно да насочат към действие, без много да се мисли. Затова ако трябва да се каже "Подреди променливите по отмествания кратни на осем байта", много по-вероятно е да се чуе едно от следните неща:
"Тези (показват се променливите с пръст) ги изравни на осем"
"Резни ги по параграф"
"Хакни бюреците осмотично"
"Алигни вариейбълите с две на трета"
И разбира се, класическото:
"Таковай ги тия с такова"

Модулност

      Модулността в програмирането всъщност не е задължителна. Възможно е една програма да бъде написана като роман - от начало до край, без да има преходи и извикване на функции. Но практиката е доказала много, ама много отдавна, че това не е целесъобразно. Тъй като един алгоритъм е поредица от действия които може да се разглеждат като отделни програми, пъти по-лесно е те да се реализират като отделни модули и да се тестват самостоятелно. Ако една програма бъде написана като отделни модули (в смисъл на функции), това води до:
  1. По-добра четливост на сорса
  2. По-лесно дебъгване и тестване
  3. Използване на вече написани модули в други програми
  4. Възможност различните модули да се пишат едновременно от няколко програмиста
  5. Възможност отделни модули да се пишат на различни езици
      Това с което трябва да се внимава, е да не се прекалява с модулирането. Както казахме, една програма може да се напише като един модул - от начало да край, може да бъде написана и като десет модула което е по-добре. Но тя може да бъде написана и като сто, и като хиляда модула, а това вече води до доста големи затруднения. Преценката, кое трябва да бъде изнесено като отделен модул и кое не, се взема от програмиста и зависи от различни фактори: Честота на използване на модула, големина на същия, четливост на програмата, искано бързодействие, стил на програмиране на самия програмист и много други.

Четливост

      Общо взето четливостта на кода има известна аналогия с четливостта на почерка на отделния човек. Както по почерка може да бъде установено от кой е написан даден текст, така и по стила на подреждане на сорса може да се установи кой програмист го е сътворил. Смятах да дам пример - една и съща програма по еднакъв алгоритъм написана от двама програмисти, но се отказах, защото ако не сте се занимавали с програмиране трудно ще доловите разликата между тях, а ако сте се занимавали, вече сте виждали достатъчно сорсове и знаете за какво говоря. Вместо това ще поясня защо четливостта на сорса е и изискване и идеология.       Ами значи, идеологията идва от това, че ако напишете една достатъчно сложна програма и два месеца не се занимавате с нея, когато отворите сорса първо трябва да си припомните какво точно става. Това отнема известно време. Известното време обаче може да се увеличи много пъти, ако програмата е написана без всякакъв умисъл за четливост. Тогава, вместо да внасяте необходимите корекции, ще се чудите за какъв дявол викате еди-коя си функция или защо онзи бит е установен. Твърдя това от личен опит.
      Изискването за четливост се поставя от шефа на фирмата към която пишете в момента. То се налага поради простата причина, че не се знае колко време програмата която пишете, ще продължи да се пише от вас. Възможно е да ви дадат друг проект, да напуснете фирмата и др., но резултата е един и същ - друг ще трябва да продължи програмата. Ако тя обаче не е четлива, другият ще загуби много време, а времето е пари, следователно фирмата ще загуби пари. Тази проста логика кара шефовете да се разхождат край програмистите и да ги карат да слагат коментари за щяло и не щяло, без да осъзнават че твърде многото коментари отвличат вниманието на четящия и никак не спомагат за четливостта. Още повече, че за да бъдат написани, коментарите трябва да се измислят, а това не е толкова просто колкото изглежда.
    Относно четливостта, мога да ви препоръчам следните неща:
     
  1. Докато все още нямате богат опит спазвайте общите принципи на езика на който пишете в момента (т.е. пишете като по учебник). Програмата ще заеме повече екрани от колкото е необходимо, но пък екранното пространство все още е безплатно.
  2. Пишете модулно.
  3. Коментирайте функциите. Преди всяка функция кажете: Тя прави това и това, и връща еди какво си. Това помага много при проследяване на основния алгоритъм, затова коментарът на функция може да бъде по-подробен.
  4. Вътре в тялото на функциите не пишете много коментари. Те отвличат вниманието, а и ако коментарът на функцията е подробен няма смисъл от тях. Коментират се по-особени неща и фигури от висшия пилотаж.
  5. Коментарите трябва да са стегнати, а не разтеглени локуми. Ако искате да пишете проза станете писател, но недейте да пишете коментари от вида "На тази прекрасна променлива забиваме стойност две". Ако прекрасният програмист четящ прекрасната ви програма, не може да разбере нещо толкова елементарно, на него трябва прелестно да му се обясни, че има още да учи.
  6. Пишете коментарите веднага след като напишете (или докато пишете) функцията. По-късно може да пропуснете нещо важно.
  7. Имайте предвид, че в системното програмиране коментарите са особено важни. При него се коментира основния алгоритъм, тъй като той е на заден план. Преди функциите може да посочвате файл от който сте взели информация за устройството което програмирате, тъй като самия файл може да съдържа грешка.
      Тези препоръки са само насочващи. Всеки програмист с опит вече е изградил собствен стил на програмиране и знае кое да коментира и кое не. Този придобит с практиката стил е негов почерк и по него може да се открият доста неща: На какъв език е започнал, какви програми е писал, приблизително от колко време се занимава с програмиране, какъв характер има (особено се забелязва чувството за хумор), каква Бира пие ако щете, и още много други неща.