რჩევები საიტის ოპტიმიზაციისთვის (Front end)

დაახლოებით ერთი თვის წინ GeOlymp-ის საიტის გარეგნული მხარე განვაახლეთ თუმცა ბრაუზერის მხარეს სწრაფად ჩატვირთვისთვის საჭირო ოპტიმიზაცია არ გამიკეთებია. ამ პოსტში შევეცდები იმ თემებზე ვისაუბრო, რასაც ამ პროცესზე აქვს გავლენა; ამასთან, რეალურად შევასრულო ისინი საიტზე და ჩატვირთვის დროები შევადარო.

ახლა ქრომი მაჩვენებს, რომ ჯეოლიმპის ერთ-ერთი მთავარი გვერდის გახსნის დროს სერვერზე 42 მოთხოვნა იგზავნება, 307.9 კილობაიტი იტვირთება და 1.2 – 2.3 წამს ანდომებს გვერდის ჩატვირთვას. შევეცდები ამის გაუმჯობესებას..

1. ნაკლები რაოდენობის HTTP მოთხოვნის გაგზავნა სერვერზე
ერთ-ერთი ყველაზე მნიშვნელოვანი წესი, რომელსაც შესამჩნევი შედეგი აქვს, სერვერზე გასაგზავნი HTTP მოთხოვნების შემცირებაა. ეს მოთხოვნები იგზავნება თვითონ html ფაილის და ასევე თითოეული კომპონენტის წამოსაღებად, რასაც საიტი შეიცავს (სურათები, css და javascript ფაილები, ა.შ.).

http მოთხოვნა საკმაოდ მძიმე ოპერაციაა, რადგან მის შესასრულებლად საჭიროა DNS სერვერთან მისვლა, დომენის შესაბამისი ip მისამართის მიღება, მიღებული მისამართის საშუალებით ჰოსტ სერვერის მიგნება და ბოლო ბოლო ვებსერვერიდან რესურსის წამოღება. თუ ეს პუნქტები გეოგრაფიულადაც დაშორებულია (მაგალითად სხვადასხვა ქვეყნებში), მოთხოვნას უფრო მეტი სერვერის და ქსელის გავლა უწევს და დრო შესაბამისად იზრდება (ილუსტრაცია: როგორ მუშაობს ინტერნეტი).

Read more

Vanilla Cage – Breaking the Cage

არ შემიძლია, პოსტი არ მივუძღვნა Vanilla Cage-ის ევროვიზიის საკონკურსო სიმღერას <3 რადგან ვამაყობ ჩემი დის შესრულებით (drums). წარმატებებს გისურვებთ გოგოებო :))

დამოუკიდებელი ტრანზაქციები

ტრანზაქციების ქვეშ ვგულისხმობ მონაცმებთა ბაზის ტრანზაქციებს, რომლებიც სხვადასხვა ოპერაციებს აერთიანებენ და ACID თვისებების დაცვით ბაზაში არსებული მონაცემების კორექტულობას უზრუნველყოფენ. ასეთი ტრანზაქციების მხარდაჭერა დღეს მგონი ყველა გავრცელებულ რელაციურ მონაცემთა ბაზას აქვს.

როდესაც ისეთი ოპერაციების ჩატარება გვინდა ბაზის მონაცემებზე, ან მრავალი ფუნქციის და პროცედურის გამოძახება, რომლებიც ერთმანეთზეა დამოკიდებული და თუ ერთგან მაინც რაიმე შეცდომა მოხდა, ყველაფერი უკან უნდა დაბრუნდეს, ყველა insert, update და delete უნდა გაუქმდეს, ასეთ შემთხვევაში ამ ყველაფერს ერთ ტრანზაქციაში მოვაქცევთ. მუშაობის დასრულების ბოლოს კი, იმის მიხედვით ყველაფერი წარმატებით შესრულდა თუ არა, ტრანზაქციას დავა-commit-ებთ, ან rollback ბრძანებით გავაუქმებთ. ამისთვის საჭიროა, რომ მთელი პროცესის განმავლობაში შეცდომის ჰენდლინგი სწორად გავაკეთოთ, მოვიფიქროთ სად დავიჭიროთ და დავამუშაოთ ხოლმე exception და სად გავუშვათ სულ გარეთ, მანამ სანამ სულ გარე ფუნქციაში არ გავა.

ანუ განვსაზღვროთ როდის უნდა დასრულდეს პროცესი  წარმატებით და როდის უნდა შეჩერდეს. შესაბამისად, ამ კონკრეტული პირობების შემთხვევაში ან სამუდამოდ შევინახავთ ცვლილებებს, ან გავაუქმებთ.

საქმე იმაშია, რომ ზოგჯერ გვჭირდება პროცესის შიგნით ზოგიერთი ნაწილი უპირობოდ დავაკომიტოთ, იმის მიუხედავად ტრანზაქცია წარმატებით დასრულდა თუ არა. მაგალითად, თუ გზადაგზა სხვადასხვა ფუნქციაში მომხდარ შეცდომებს ცხრილში ვინახავთ, გვინდა რომ ისინი ყველა შემთხვევაში შენახული იყოს. მაგრამ როდესაც მათ შესანახად კომიტს გავუშვებთ, ის იმ ტრანზაქციის ფარგლებში შესრულებულ ყველა ოპერაციას დააკომიტებს, ამიტომ სხვა გზა უნდა ვიპოვოთ.

თუ ასეთი სახის ამოცანა ორაკლში უნდა გადავჭრათ, მაშინ შეგვიძლია ზოგიერთი ტრანზაქცია მთავარი ტრანზაქციიდან დამოუკიდებლად გავუშვათ. ამისათვის ჩვენი ქვეპროგრამის declaration სექციაში (სადაც სხვადასხვა ცვლადების ან ქვეპროგრამების აღწერა ხდება), უნდა ჩავუწეროთ pragma autonomous_transaction;

pragma კომპილატორის დირექტივაა. მისი საშუალებით შეგვიძლია კომპილატორს ინფორმაცია მივაწოდოთ და ამით ქვეპროგრამის მუშაობის გზა შევცვალოთ. ორაკლში ასეთი კიდევ სამი სახის ინსტრუქცია არსებობს exception_init, restrict_references, serially_reusable, თუმცა მათ ახლა არ ჩავუღრმავდები.

მაგალითად, თუ ჩვენს log_error პროცედურაში (რომელიც ზევით არსებულ პროცესის ნაწილია და გზადაგზა შეცდომებს ინახავს ცხრილში) ჩავამატებთ autonomous_transaction პრაგმას, მასში გამოძახებული commit და rollback ბრძანებებს არ ექნება გავლენა მთავარ ტრანზაქციაზე და მხოლოდ ქვეპროგრამის ცვლილებებს შეინახავს. ცხადია, ეს დამოუკიდებლობა ვრცელდება მთავარ ტრანზაქციასთან დაკავშირებულ რესურსებსა და lock-ებზეც.

pragma autonomous_transaction-ით შეგვიძლია ანალოგიური შესაძლებლობა მივცეთ ორაკლის ტრიგერს, რომელშიც ჩვეულებრივ არ შეიძლება დაკომიტება, რადგან ის აღმძვრელ ოპერაციაზეა დამოკიდებული (ანუ რის გამოც მოხდა ტრიგერის გამოძახება). შესაბამისად, თუ ეს უკანასკნელი დაკომიტდა, ჩვეულებრივი ტრიგერიც დაკომიტდება და ასევე როლბექის შემთხვევაში.

დამოუკიდებელ ტრიგერში ამის გარდა შესაძლებელი გახდება DDL ბრძანებების შესულება (ცხრილის შექმნა, ცვლილება, ა.შ.).

MS SQL მონაცემთა ბაზაში, როგორც ჩანს, არ არსებობს ასეთი დამოუკიდებელი ტრანზაქციების ანალოგი (ყოველ შემთხვევაში ძველ ვერსიებში არ ჰქონდათ მისი მხარდაჭერა), მაგრამ არსებობს შემოვლითი გზები, რითიც ასეთი მიზნის მიღწევა შეიძლება.

MySQL-საც არ აქვს დამოუკიდებელი ტრანზაქციების ცნება, თუმცა აქ საქმე ცოტა სხვაგვარადაა. ზევით ნახსენებ ბაზებისგან განსხვავებით MySQL-ში საერთოდ ტრანზაქციების არსებობა მის storage engine-ებზეა დამოკიდებული. ასე რომ, თუ ვთქვათ შეცდომების ლოგირება გვინდა რომელიმე ცხრილში, შეგვიძლია იმ ცხრილისთვის MyISAM ძრავი გამოვიყენოთ რომელსაც არ აქვს ტრანზაქციების მხარდაჭერა, და როლბექის დროს ამ ცხრილში არსებულ ჩანაწერებს არაფერი მოუვა.

უდიდესი საერთო ქვემიმდევრობის პოვნის ამოცანა

წინა პოსტში მაგისტრატურის გამოცდა ვახსენე და დღეს მისთვის მოსამზადებლად მეორე ალგორითმს გავიხსენებ. ფსევდო კოდები და მაგალითები ისევ ”Introduction to Algorithms” წიგნიდან ავიღე 🙂

”უდიდესი საერთო ქვემიმდევრობის” (Longest Common Subsequence) პოვნა დინამიური პროგრამირების კიდევ ერთი კლასიკური ამოცანაა. ვთქვათ მოცემულია მიმდევრობები:

X = (A, B, C, B, D, A, B)
Y = (B, D, C, A, B, A)

ამ შემთხვევაში მათი ერთ-ერთი უდიდესი საერთო ქვემიმდევრობაა მიმდევრობა (B, C, B, A)

ცოტა უკეთ განვსაზღვროთ, თუ რა არის ქვემიმდევრობა:
ვთქვათ მოცემულია მიმდევრობა X = (x1, x2, … , xm).
Z = (z1, z2, …. , zk) არის მისი ქვევმიმდევრობა, თუ არსებობს (i1,i2..,ik) X-ის ინდექსების ისეთი მკაცრად ზრდადი მიმდევრობა, რომ ყველა j=1,2,… k მნიშვნელობისთვის სრულდებოდეს ტოლობა xij = zj.

არც ამ ამოცანის ამოსახსნელად გამოგვადგება ყველა ვარიანტის სრული გადარჩევა, რადგან ექსპონენციალურ დროს მოითხოვს და დიდი მიმდევრობებისთვის პრაქტიკულად ვერ მივიღებთ პასუხს.
თუ დავაკვირდებით მიმდევრობებს, გამოჩნდება, რომ დიდი ამოცანა შეგვიძლია დავყოთ ქვეამოცანებად, სადაც ქვეამოცანა შეესაბამება მოცემული მიმდევრობების პრეფიქსთა წყვილებისთვის უდიდესი საერთო ქვემიმდევრობის პოვნას.

ინტუიციურია, მაგრამ მაინც განვსაზღვროთ მიმდევრობის ”პრეფიქსის” ცნება. თუ მოცემული გვაქვს მიმდევრობა X = (x1, x2, … , xm), მისი i-ური პრეფიქსი იქნება X-ის პირველი i ცალი ელემენტისგან შემდგარი მიმდევრობა, ანუ Xi = (x1, x2, … , xi). რა თქმა უნდა i4 = (A,B,C,B). X0 ცარიელი მიმდევრობაა.

სანამ ამოცანის ამოხსნაზე გადავალთ ერთი პატარა თეორემა უნდა დავამტკიცოთ.
თეორემა:
მოცემულია X = (x1, x2, … , xm) და Y = (y1, y2, … , yn) მიმდევრობები. ხოლო Z = (z1, z2, … , zk) არის ამ მიმდევრობების უდიდესი საერთო ქვემიმდევრობა.
1. თუ xm = yn, მაშინ zk = xm = yn და Zk-1 არის Xm-1 და Yn-1 პრეფიქსების უსქ (უდიდესი საერთო ქვემიმდევრობა).
2. თუ xm !=yn, მაშინ zk != xm-დან გამომდინარეობს, რომ Z არის Xm-1 და Yn პრეფიქსების უსქ.
3. თუ xm !=yn მაშინ, zk != yn-დან გამომდინარეობს, რომ Z არის Xm და Yn-1 პრეფიქსების უსქ.

დამტკიცება საკმაოდ მარტივია.
1. დავუშვათ საწინააღმდეგო და ვთქვათ, რომ zk != xm. რადგან xm = yn, Z-ს შეგვიძლია მივამატოთ xm ელემენტიც. ამ შემთხვევაში კი გამოვა რომ უდიდესი საერთო ქვემიმდევრობა Z-ზე გრძელია და წინააღმდეგობას მივიღებთ. მოკლედ რომ ვთქვათ, თუ მოცემული მიმდევრობების ბოლო ელემენტები ერთნაირია, ის ელემენტი აუცილებლად იქნება უსქ-ის ბოლო ელემენტი.

დებულების მეორე ნაწილიც იგივე პრინციპით დავამტკიცოთ. ვთქვათ Zk-1 არ არის Xm-1 და Yn-1 პრეფიქსების უსქ. მაშინ იარსებებს რაიმე უსქ W, რომლის სიგრძეც k-1-ზე მეტია. მას რომ მივამატებთ xm = yn ელემენტს, ის k-ს გადააჭარბებს. ეს წინააღმდეგობაა.

2. რომ არსებობდეს Xm-1 და Y-ის უდიდესი საერთო ქვემიმდევრობა – W, რომლის ელემენტების რაოდენობა k-ზე მეტია, ის აგრეთვე იქნებოდა X-ის და Y-ის უსქ და თან Z-ს გადააჭარბებდა სიგრძეში. მოცემულობის მიხედვით კი Z არის X-ის და Y-ის უსქ.

3. მეორე პუნქტის ანალოგიურად მტკიცდება.

ამ თეორემიდან კარგად ჩანს რომ ორი მიმდევრობის უდიდესი საერთო ქვემიმდევრობა თავის თავში შეიცავს ამ მიმდევრობების პრეფიქსების უდიდეს საერთო ქვემიმდევრობას და მის საპოვნელად რეკურენტული ფორმულა შეგვიძლია შევადგინოთ.

თუმცა ცხადია, რომ პირდაპირი რეკურსია სრულ გადარჩევას გააკეთებს, რადგან მისი განშტოებები ძალიან ხშირად ერთსა და იმავე ქვეამოცანას ამოხსნიან. ამიტომ, როდესაც დავადგენთ უდიდეს ქვემიმდევრობის სიგრძეს თითოეული პრეფიქსისთვის, დავიმახსოვროთ ის და მეორედ აღარ მოვძებნოთ იგივე პრეფიქსის უსქ.

კონკრეტულად, შემოვიღოთ რიცხვითი ტიპის ორგანზომილებიანი მასივი c და მის [i][j] უჯრაში შევინახოთ Xi და Yj პრეფიქსების უსქ-ის სიგრძე.

როცა i ან j ნოლის ტოლია მაშინ c[i][j]-ის მნიშვნელობა ნოლის ტოლი იქნება რადგან საერთო ქვემიმდევრობაც ცარიელი გამოვა პრეფიქსის მსგავსად.

როცა i, j >0 და xi = yj, ეს ნიშნავს რომ Xi და Yi მიმდევრობების უსქ-ის სიგრძე ერთით მეტია ვიდრე Xi-1 და Yi-1 მიმდევრობების უსქ-ის სიგრძე, რადგან ერთი კიდევ ერთი საერთო ელემენტი დაემატა.

როცა i,j>0 და xi != yj, ეს ნიშნავს რომ უსქ-ის სიგრძე იგივეა რაც Xi-1 და Yj პრეფიქსების ან Xi და Yj-1 პრეფიქსების უსქ-ს ჰქონდა. ამიტომ ამ ორიდამ მაქსიმალური სიგრძისა ავირჩიოთ და მისი მნიშვნელობით შევავსოთ c[i][j] უჯრა.

საბოლოოდ ასეთი რეკურენტული ფორმულა მივიღეთ:

თუმცა საბოლოო პასუხი არა მხოლოდ უდიდესი საერთო ქვემიმდევრობის სიგრძეა, არამედ თვითონ ქვემიმდევრობა. ამიტომ შემოვიღოთ c-ის ზომის კიდევ ერთი დამატებითი ორგანზომილებიანი მასივი b, რომლის [i][j] უჯრაში შევინახავთ ხოლმე თუ რომელი მეთოდით შევავსეთ c[i][j].

ფსევდო კოდი ასე გამოიყურება:

კოდში კარგად ჩანს რომ ორი ჩადგმული ციკლი მუშაობს. მათგან ერთი m, ხოლო მეორე n ცალ იტერაციას აკეთებს. თითო უჯრის მნიშვნელობის გამოთვლა O(1) დროში ხდება. ანუ ალგორითმი O(mn) დროში მუშაობს, სადაც m და n მოცემული მიმდევრობების სიგრძეებია.

ვნახოთ როგორ შეავსებს ეს ფუნქცია მასივებს ჩვენს მაგალითზე დაყრდნობით:

მხოლოდ ისღა დაგვრჩენია რომ ამ მონაცემებიდან უდიდესი საერთო ქვემიმდევრობა აღვადგინოთ. ამისთვის პატარა რეკურსიული ფუნქცია გამოვიყენოთ.

ეს ფუნქცია O(m + n) დროში მუშაობს, რადგან ის ყოველ ჯერზე მინიმუმ ან i-ის ამცირებს ერთით, ან j-ის, ან საერთოდ ორივეს ერთად.

შევაფასოთ გამოყენებული მეხსიერებაც. ამ ალგორითმში ორი ცალი m x n ზომის მასივი გვაქვს, ამიტომ O(mn) მეხსიერებას ვიყენებთ. შეიძლება b მასივი საერთოდ გამოვტოვოთ, თუმცა მეხსიერების O(mn) შეფასება არ შეიცვლება.
საქმე იმაშია, რომ c[i][j] უჯრის მეზობელ c[i-1][j-i], c[i][j-1], c[i-1][j] უჯრებს თუ დავათვალიერებთ, მარტივად მივხვდებით რომელ მათგანზე დაყრნობით მივიღეთ c[i][j] მნიშვნელობა და აღარ დაგვჭირდება ამის b მასივში შენახვა.

განახლება კომენტარიდან:

Zuba Dalama:
ერთ-ერთი “real world” მაგალითია diff: http://en.wikipedia.org/wiki/Diff

ვისაც აინტერესებს თუ როგორაა რეალიზებული იხ. google–ის opensource პროექტი (წარმოდგენილია შემდეგ ენებზე: Java, JavaScript, Dart, C++, C#, Objective C, Lua და Python) http://code.google.com/p/google-diff-match-patch/
და მისი დოკუმენტაცია: http://neil.fraser.name/writing/diff/.

მატრიცათა მიმდევრობის გადამრავლების ამოცანა

გადავწყვიტე წელს უნივერსიტეტში დავბრუნდე მაგისტრანტის სახით, მაგრამ ამისთვის ჯერ გამოცდა უნდა ჩავაბარო რამდენიმე დღეში. საკითხების სიაში დინამიური პროგრამირების კლასიკური ამოცანებიც შედის, რომელთაგან ერთ-ერთი ”მატრიცათა მიმდევრობის გადამრავლების” ამოცანაა (Matrix chain multiplication). ამ პოსტში მისი ამოხსნის შესახებ დავწერ. მაგალითები და ფსევდო კოდები Introduction to Algorithms წიგნიდან ავიღე.

სანამ ამოცანის დასმაზე გადავალ, მოკლედ გავიხსენოთ მატრიცების გადამრავლება. ეს არის ბინარული ოპერაცია, როდესაც ორ მატრიცაზე ოპერაციების ჩატარებით ვიღებთ ახალ, მესამე მატრიცას.

აქ ვხედავთ, რომ გამრავლებისთვის აუცილებელია პირველი მატრიცის სვეტების რაოდენობა მეორე მატრიცის სტრიქონების რაოდენობას ემთხვეოდეს. შესაბამისად, თუ პირველი მატრიცის განზომილებებია p და q, ხოლო მეორე მატრიცის – q და r, მათი გადამრავლებისთვის pqr ცალი წრფივი გამრავლების ოპერაცია დაგვჭირდება.

მატრიცების გამრავლების ოპერაცია ასოციაციურია, ანუ (AB)C = A(BC), მაგრამ არა კომუტატური – AB ნამრავლის შედეგი შეიძლება არ იყოს BA ნამრავლის შედეგად მიღებული მატრიცის ტოლი.

ვთქვათ მოცემულია A1,A2,A3 მატრიცების მიმდევრობა, სადაც A1 მატრიცის განზომილებებია 10 x 100, A2 – 100 x 5,   A3 – 5 x 50. იმის მიხედვით, თუ რა თანმიმდევრობით გავამრავლებთ მატრიცებს, სხვადასხვა რაოდენობის ოპერაცია დაგვჭირდება, თუმცა ასოციაციურობის გამო საბოლოო შედეგი ყოველთვის ერთი და იგივე მატრიცა იქნება. მაგალითად:


Read more