Fast search using hashing.

ZXNet echo conference «code.zx»

From Aleksey Malov To All 30 March 2000

Greetings, All! Initially, this article was intended for the ZX-Pilot newspaper, but, because... about fate I haven’t heard anything from this newspaper for six months, so I decided to throw it here. I think It will be useful for you to read too. So, we will talk about such a useful thing as quickly searching for information on example of searching for assembler tags in the table. This example was not selected by chance - at the moment your humble servant is studying (don’t think something bad) by writing your own assembler. Let's skip the reasons why I decided to make my own assembler, but immediately let's get down to business. So, each mark has its own name and meaning, for the sake of which it is necessary search for a label throughout the table. Objective: to ensure that the label is entered into the table as quickly as possible the first pass and find it as quickly as possible on the second pass. Solution 1. Dumb and the first one that came to mind. Sequential method search. The memory for the tag is distributed as follows: +0 byte - length of the label name (1-15) and all sorts of flag bits. +1..+[+0] - symbols of the label name +[+0]+1..+[+0]+2 - label value. The search algorithm is simple: 1. Compare the length of the desired label and the current one, if they are not equal, then repeat step 1 for the next mark. 2. We compare the symbols of the labels in approximately the same way: ;sp - address in the label table ;cf=0pop hl; popped 2 characters of the current label from the stack ld de,nn;nn - symbols of the searched label sbc hl,de;16-bit cp jr nz, point 1; if not equal then point 1 3. If found, then take the value of the label, otherwise - bummer. It is not difficult to calculate that even with such a fast method of comparing characters marks (42 measures per word) with the number of marks of length equal to the length of the searched one marks equal to 100, the search time will be about 6300 cycles only on comparison of symbols. I did not take into account the time of screening by length and all sorts of recalculations. That's why I decided to use a much faster method. Solution 2. Hashing method with linear collision avoidance probe method. I’ll say right away that I didn’t come up with this myself, but heard it at lectures on structures and algorithms for data processing. So, what is the hashing method based on? Hashing is the calculation of the address of an element in a table using its key. So, for us, the role of an element is played by the assembler label. In your In this case, this could be either a phone number, or a word in the dictionary, or maybe one of the fields in the database. We will consider the key to be a number that is calculated based on one or more element fields. In our case, the key value calculated from the label name. It remains to decide on the function by which the calculation will take placekey (it is usually called a HASH FUNCTION). There's a lot of room for your imagination. Requirements that a hash function must satisfy: 1. It must be calculated simply and elegantly. You can even use table. 2. It should show an approximately even distribution of numbers keys regardless of the field name. In our case, no matter what labels are used (name length and characters). probability of receiving keys should be approximately the same. A good hash function is the algebraic sum of the character codes of the label name. The resulting key is calculated using the formula: k=H mod N, where H is a hash function, N is a natural number. As N It is convenient to use the number 2^n, where n is also a natural number. In this In this case, everything is much simpler: k=H and (2^n-1). Now let's move directly to the algorithm for creating and searching for tags. Labels are stored in memory as follows: In one memory area, the actual marks themselves are stored, stored as in sequential search method The addresses of these same tags are stored in another area. This table is preceded by a small table n bytes long, which stores the number tags with the same key. Unlike the previous table, the address table (when creating a label table) is not filled in sequentially, but according to the following algorithm: 1. Based on the label name, its key is calculated. 2. In the table preceding the table of addresses, we look at how many labels with a given key. Next, you need to check whether a label with the same name exists in table to avoid duplicate labels: ;SP- ADDRESS IN ADDRESS TABLE ;B-LENGTH OF THE CREATED MARK ;C=15 POP HL; TAKE THE ADDRESS OF THE MARK LD A,(HL); CONTAINS LABEL LENGTH AND FLAGS (USED, DEFINED, ETC) AND C CP B; COMPARE THE LENGTH JR Z,CHECK_SYMBOLS;IF THE LENGTHS OF THE FOUND AND CREATED MARKS COINCIDE, THEN ;COMPARING THEIR SYMBOLS ... TOTAL - 35 BARKS PER MARK. If a label already exists in the table, then generate an error of type label exists. Otherwise, we create a new label and add its address to the address table. Searching for a label (especially the inner loop search) is basically the same as creation, with the only difference that you don’t need to create anything. Memory consumption: Each label descriptor consumes 2 bytes, but not one should naively believe that the total amount spent will be (2*number labels + total length of label names) bytes. It will actually be spent somewhat more, since some keys will appear more often than others, so the memory costs will be like this: (2*N*M+total length of tag names), where M is the number of tags with the most frequently occurring key.However, despite this, the game is worth the candle. Here is a specific example: when number of labels with a length from 2 to 11 characters equal to 522, the following were obtained results: N Len C 4 1104 138 8 1184 74 16 1280 40 32 1536 24 64 1920 15 128 2816 11 256 4096 8 For those who do not quite understand: N is a parameter from the hash function, Len is the length tag address tables, C - maximum number of tag search cycles. Number effective search cycles (i.e. those in which it is necessary to compare tag symbols) is several times less than the average number of search cycles, because First, the lengths of the marks are compared, and then, if the lengths match, the symbols are compared. In addition, according to probability theory, the average number of search cycles will be somewhere 2 times less than indicated in the table. In total, you will have to compare symbols for about 20% of the labels located in the table. The results are really impressive: with N equal to 256, the Average number of labels, which must be checked to find (or not find) a mark, about 256 times less than the number of labels in the table. Cool, right? I will most likely make it possible to manually select the parameter N, so that owners of cars with large memory sizes can be sure that that the benefit from their memory will be not only in its quantity, but also in speedsearch for marks, because they will be able to afford to set the N parameter to 256. Perhaps, if I can quickly perform calculations, it will be introduced hashing search method based on name collision resolution method Quadratic Trial, which reduces the number of attempts to find a mark in C^(1/2) times compared to the Linear Sample method, where C is the number from the table. If anyone has suggestions for improving the algorithm, do not hesitate to write. P.S. Theoretically, the speed of assembly of commands like ldi, neg, cpl, nop will be no more than: on the first pass - 80 cycles/command. on the second pass - 160 cycles/command. I wish you health, happiness and creative Uzbeks. Aleksey Malov aka VIVID/Brainwave.

From Aleksey Malov To Dmitry Lomov 3 April 2000

Greetings, Dmitry! Thu 30 Mar 2000 at 22:33:04 Dmitry Lomov and Aleksey Malov were talking on the topic Quick search using hashing.. DL> in vain. DL> what are the main ideological points of the intended assembler? Convenience in terms of interface (I don’t like Alasm because of this), availability local tags (useful thing), high speed (Zasm infuriates), and support my 1Mb Simm (No one except Alasm understands above 128k, but Alasm not convenient). Well, the only thing Storm lacks is a multicolor screensaver ;). AM>> A good hash function is the algebraic sum of character codes AM>> label name. DL> bad, bad, you can trust me. I won’t justify it theoretically (yes DL> and it won’t work ;) , I’ll just say that if I use only in labels DL> large Latin letters and numbers, the effectiveness of the method will drop by half DL> seven. It's better to use XOR with cyclic shifts. when I do this DL> I was studying, I went through many options... I settled on this one. It is planned to store tags in tokenized form, because the following character sequences: loop, LOOP, ER,er, bank,BANK, ETC.occur quite often and will save on memory and search speed somewhat will increase, and this will result in a more or less decent spread of keys. DL> XOR first DL> RRCA DL> XOR second DL> RRCA DL> ... We must try... DL> the best option is to calculate the CRC of all letters of the label, possibly including DL> there is also a length byte. option - CRC8, the optimal polynomial is calculated Could you tell us more about the CRC calculation algorithm? DL> provide a detailed description of your method, and in return you will receive mine ;) Memory for tags is planned to be allocated as follows: the labels, along with their values, grow sequentially from top to bottom. pointers to the labels are located below: [memory for m pointers to labels with key 0] [memory for m pointers to labels with key N-1] m is a power of two. The search is like this: 1. Calculate the key for the desired tag using some algorithm. 2. Next, look in a separate table to see how many pointers to labels there are with with this key. 3. set sp to the beginning of the table of pointers to labels with this key. 4. B is the length of the desired mark. 5. in C-#0f, because ml. The 4 bits of the label flag store its length. 6. and let's go compare the lengths of the markspop hl ;take the address of the label, it contains the length of the label, and immediately after it - ;symbols and meaning. ld a,(hl) ;take the length and c ;discard unnecessary bits cp b ;compare lengths jr z,check_symbols, if they match, then compare the symbols ..... check_symbols inc hl; go to label symbols ld de,adr; address of the symbols of the searched label xor 15 ;15-a ldc,a add a,a add a,c ld c,#0f;restored c add a,a;a*6 ld(jmr+1),a jmr jr nz,$ ld a,(de) cp(hl) jr nz,oblom ;tags did not match, compare others inc e inc hl ... oblom let's start searching for the trace. tags. A few words about creating labels. As soon as the number of labels with one key exceeds the size allocated for pointers memory will open the next block of memory (possibly in a different page) for labels. Somewhat uneconomical, but the assembler was originally planned for computers with memory more than 128 kg. In my test, I used labels from the file with labels for Zasm3.10 (authors In their naivety, they thought that people would write overlays for their monster). The labels are all in capital letters. their length ranges from 2 to 11 characters. (Most the long label I used was 13 characters: GOPS_MUST_DIE ;) I hope no one was offended? ;)) DL> according to my estimates, 50...100 times for N=256. if I understand correctly, NDL> is directly related to the key bit depth, right? 2^(bit size)=N? And you get it right the first time ;). AM>> Perhaps, if I can do the calculations quickly, AM>> a search method with hashing based on the method will be introduced AM>> eliminating collisions of the Quadratic Sample name, DL> everything is clear ;) But I personally don’t really understand the method of this test, maybe you can explain? ;-))) AM>> P.S. Theoretically, the speed of assembly of commands like ldi, neg, cpl, AM>> nop will be no more than: on the first pass - 80 cycles/command. Look: lnbeg ld a,(hl) ; line length 7 inc hl ;13 dec a ;17 jr z,lnbeg;24 jp m,error;34. 7 bits of string length - a sign of a syntax error ld b,a ;38 ld a,(hl) ;46.the first thing in the line cp 15 ;52 jp nc,command1;62 ;processing of labels, directives like ".", "+", "-", comments command ld a,(hl) command1 inc hl ;68 cpopers;75 jr nc,n_operands;82 processing commands with operands exx;86 inc de;de-$;92 cp onebyte ;99 jr c,$+3; ;106 bypass if single-byte command inc de ;112 exx ;116 djnz command;124 next command separated by colonjp lnbeg ;134 total: 134 measures DL> will not work, you can trust me. realistically - about 200. Well, okay, 134 bars - I was bragging from memory ;) But if after a colon To write such commands, it will take about 70 cycles m/. AM>> on AM>> second pass - 160 clock cycles/command. DL> and in general - it should be one-pass ;) what is the second pass to the bathhouse? Yes, I have a hard time imagining how all sorts of forward references are made there, maybe throw in an idea. I wish you health, happiness and creative Uzbeks. Aleksey Malov aka VIVID/Brainwave.

From Aleksey Malov To Dmitry Lomov 4 April 2000

Greetings, Dmitry! In principle, I will most likely store pointer blocks in the form of records, consisting of the following fields: 1 number of pointers to labels with a given key 2 array of m pointers. m= min (256, 4096/N), here N=2^(key capacity). 3 pointer (page and address) to the next entry with the same key. New a record will be created if the number of pointers in this record will exceed the size of the array in this entry. ┌───────────────────────────────┐ ┌─────┐ │number of pointers with key 0 │ │ │ ├───────────────────────────────┤ │ │ │pointers to marks ├────────>│ ├─────> ... └───────────────────────────────┘ └─────┘ ..... ┌───────────────────────────────┐ │number of pointers with key N-1 │ ├───────────────────────────────┤ │pointers to marks ├───────>.... └───────────────────────────────┘ I wish you health, happiness and creative Uzbeks. Aleksey Malov aka VIVID/Brainwave.

From Dmitry Lomov To Aleksey Malov 5 April 2000

Hello, Alexey! One day, Mon Apr 03 2000 23:45, Aleksey Malov wrote to Dmitry Lomov about [Quick search using hashing.]: AM> Convenience in terms of interface (I don’t like Alasm because of this), AM> presence of local labels (useful thing), high speed (Zasm AM> infuriates), s ;) AM> and support for my 1Mb Simm (No one except Alasm above 128k AM> does not understand the route, but Alasm is not convenient). Well, in Storm only AM> multicolor screensaver is not enough ;). Hehe. AM> It is planned to store labels in tokenized form, because the following AM> character sequences: loop, LOOP, ER,er, bank,BANK, AND AM> T.P. occur quite often and save on memory and speed AM> search will increase slightly, and because of this it will turn out more or less AM> decent spread of keys. yeah, this is clearly overkill. no need to do that ;) Tokenization of tags is, of course, necessary, but not like that. DL>> the best option is to count the CRC of all letters of the label, perhaps DL>> including the length byte. option - CRC8, optimal polynomialDL>>calculated AM> Could you tell us more about the CRC calculation algorithm. Now I hardly remember my calculations and developments. but I have the following ready to go ;) ─ RU.ALGORITHMS (2:5030/255.27) ────────────────────────────── RU.ALGORITHMS ─ Post: 145 of 237 Scn From: Den Soroka 2:4641/121.256 10 Jan 99 21:40:00 To: Denis Korablev 14 Jan 99 02:27:46 Subject: CRC ─────────────────── ──────────────────── ──────────────────── ──────────────────── Good day Denis! Somehow Fri Jan 08 1999, DENIS KORABLEV trampled the keys for all!: DK> Sorry for the stupid question, but how to count the subject? 1. Let us be given a bit series 101110011... of length L bits. Let's imagine this series as a polynomial L-1 L-2 L-3 A=1* 2 + 0*2 + 1*2 + ... Now let's take any number less than L (1010) in length and imagine it similarly in the form of a polynomial B. Now let’s calculate the remainder when dividing A by B. This remainder will be an analogue of the CRC code. 101110011 | 1010 -1010------- ---- |100101 1100 -1010 ---- 1011 -1010 ---- 1<----remainder2. Now if instead of subtraction you use XOR then this will be a calculation real CRC code 101110011 | 1010 xor 1010 ------- ---- |100111 1100 xor 1010 ---- 1101 xor 1010 ---- 1111 xor 1010 ---- 101<----CRC code 3. XOR for each bit - very slow, so you can calculate it in advance code for each combination of n bits (in real algorithms n=8), for our example n=4. *** ---- pad n with zeros | 0001 0000 | 1010 1 010 ------- ---> 0001 corresponds to 0100 ----- | 00010 100 *** 0011 0000 | 1010 10 10 ------- ---> 0011 corresponds to 0110 ----- | 00111 1 100 1,010 ----- 1100 1010 ---- 110 *** Then we get 0001 0111 0011 | xor ----0100 x ----o 0011 r | ----0110 ---- 101<----- CRC code (coincides with the one already calculated, no matter how strange :) 4. Any number can be taken as number B, but there are numbers for which have been proven to provide the greatest likelihood error detection: CRC-16/CITT: 1021h XMODEM: 8408h ARC: 8005h CRC-32: 04C11DB7h 5. You can read in more detail and strictly on the Internet about ftp.adelaide.edu.au/pub/rocksoft/crc_v3.txtDenis Soroka -+- + Origin: Den's Lair, Zaporozhye, Ukraine (FidoNet 2:4641/121.256) DL>> provide a detailed description of your method, and in return you will receive mine ;) AM> memory for tags is planned to be allocated as follows: AM> labels along with their values grow sequentially from top to bottom. AM> pointers to labels are located below: AM> [memory for m pointers to labels with key 0] AM> .... AM> [memory for m pointers to labels with key N-1] AM> m - power of two. oh how bad :( such a decision makes sense only when you have already made all other decisions has reached the theoretical speed limit, but you need more ;) in general, you should never make large fixed buffers, and even such a hole as yours. small buffers in your case - sax, it’s not clear, what to do if they overflow. STORM 1.3: a 256-word table that stores links to the first elements chains of labels that correspond to the same key (eight-bit). each label contains length, name, value, flags and a link to the next one a mark in the chain (or a sign at the end of the chain). AM> Search like this: AM> 1. Calculate the key for the required label using some algorithm. AM> 2. Next, look in a separate table to see how many pointers there are AM> to labels with this key. what for? It's easier to make an end marker. AM> 3. set sp to the beginning of the table AM> pointers to labels with this key. similarly, only I have this address in the table. AM> A few words about creating labels. AM> As soon as the number of labels with one key exceeds the size of the allocated under AM> memory pointers will open the next memory block (possibly in another AM> page) under the labels. Somewhat uneconomical, but the assembler is originally AM> was planned for computers with more than 128kb of memory. but I put such a fierce solution in storm2 ;) only there is much more more ferocious, the search is simply lightning fast. according to estimates - two to three times faster than you have :-) AM>>> P.S. Theoretically, the speed of assembly of commands like ldi, AM>>> neg, cpl, nop will be no more than: on the first pass - 80 AM>>> cycles/command. AM> See: AM> lnbeg ld a,(hl) ; line length 7 AM> inc hl ;13 AM> dec a ;17 AM> jr z,lnbeg;24 AM> jp m,error;34. 7 bits of string length - a sign of syntactic AM> errors length of the line at the beginning? But how are you going to go back through the text? ;) AM> ld b,a ;38 yeah, it's not the length of the line, it's the number of commands in the line? yes, memory for text you don't regret :-) losing a whole byte in a line for unjustified savings time is not best. AM> ld a,(hl) ;46.the first thing in the line AM> cp 15 ;52 AM> jp nc,command1;62 AM> ;processing of labels, directives like ".", "+", "-", comments AM> .... AM> command ld a,(hl) AM> command1 inc hl ;68 AM> cp opers;75 AM> jr nc,n_operands;82 processing commands with operands AM> exx;86 AM> inc de;de-$;92 AM> cp onebyte ;99 AM> jr c,$+3; ;106 bypass if single-byte command AM> inc de ;112 AM> exx ;116 AM> djnz command;124 next command separated by colon AM> jp lnbeg ;134 AM> total: 134 measures DL>> it won't work, you can trust me. realistically - about 200. AM> Well, okay, 134 bars - I was bragging from memory ;) But if after AM> colon to write such commands - then about 70 clock cycles will be m/. and generate the code? and the marks? I think "on average" :-) what if the jar runs out? what if the code buffer overflows? and if in memory instead text garbage? should I increase the number of the compiled line? and the number of the operand in the line increase? and for the second pass generate separate procedures that will do all this... yes, but is the text format strong? is everything reaped? how to navigate through the text editor will it be? I just worked on the text format for several months, taking into account the speed compilation and source volume... DL>> and in general - it should be one-pass ;) what, in the bathhouse, the second DL>> passage? AM> Yes, I have little idea how there are all sorts of forward links AM> are being done, maybe you can throw in an idea.I had such ferocious ideas that I could just hang myself... but some of them some had to be shot - one person will never write this. brain more resourceful than hands ;) in the general case, a list is maintained for expressions that are not currently evaluated, containing themselves, the place where the value should be written, and the method entering this value. Best wishes. Dmitry.

From Aleksey Malov To All 22 April 2000

Greetings, All! So what do we have using hashing? It is easy to see that even with the most ideal method of calculating the hash function the maximum number of labels with the same key will be no less than C/(2^N), where C is the number of tags, N is the key bit size. In a real situation, this number is equal to approximately C/(2^(3*N/4)). Those. for 512 tags with N=8 this number will be about 8. For 1024 marks - 16. For 4096 - 64. If we use a tree, then what will be the search criterion? After all, we are looking for not numbers, but symbols of marks. If we represent label symbols as N-bit numbers, then the lion's share of time will be spent comparing 96-bit numbers (if the label length is no more than 16 characters and each character occupies 6 bits). B In the case of 512 labels, the weight of the tree will be no more than log 512 =9, i.e. with the number of marks less than 512 we lose to the hashing method. But already with the number of marks equal 1024 the number of search cycles will be 10. But when adding a new label it is necessary to ensure that the maximum difference in tree vertex levels was no more than 1. Otherwise, it is necessary to do balancing, i.e. throw pointers to vertices. In short, you need to come up with a search method that is realistic for the task at hand, and namely: 1. Adding tags occurs in random order, i.e. data comes in unsorted order. 2. Removing labels from the table is not required.3. It is not advisable to have more than 2 pointers attached to each label (memory must be saved). 4. Practical search speed should be high. 5. Everything will be implemented on the Spectrum in ASSEMBLY language. Therefore, to everyone algorithms that are more suitable for C and Pascal are better treated with caution. So offer your ideas (it is advisable that they be justified and satisfied the conditions of the task). At least there will be some benefit. This better than arguing about whether the C64 can use a floppy drive controller run several Amiga workbench's and at the same time play at speed 25 fps porn animations in MPEG format, downloaded in Real time from inet the help of a 2400 modem (if someone is still tormented by this question, then I will answer - won't be able to, even if Windows stops glitching, and Bill Gates bequeaths everything to me his fortune, and Slava Mednonogov will complete ChV-2 by the beginning of the Last Judgment). I wish you health, happiness and creative Uzbeks. Aleksey Malov aka VIVID/Brainwave.

From Dmitry Lomov To Aleksey Malov 29 April 2000

Hello, Alexey! Once, Sat Apr 22 2000 00:25, Aleksey Malov wrote to Dmitry Lomov about [Quick search using hashing.]: DL>> but I put such a fierce solution in storm2 ;) only there DL>> is much more ferocious, the search is simply lightning fast. according to estimates - times in DL>> two or three faster than yours :-) AM> Maybe I can do it even faster ;)) it won't work ;) DL>> length of line at the beginning? but how will you go back through the text? DL>> ;) AM> Listen here: Each text on the page is no more than 16k and can consist of AM> of no more than 8192 lines. A small is stored along with the text. AM> a 64-byte plate containing row addresses, s AM> numbers divisible by 256. very difficult. but the idea itself is interesting. I didn't think of that before :) AM> [6] [LD] [A,B] [C,NUMBER] [DECIMAL BYTE] [7] AM> ^ AM> ║ AM> ║ AM> ╚═ string length AM> TOTAL: 6 BYTES per line. funny. my version matches this by 90% ;) AM> The text will occupy one jar (the number of texts will be limited AM> number of banks). bad. The expansion time of the 16k array is quite noticeable during operation. AM> Garbage will be checked simply - since we know the addresses AM> every 256 rows, it will not be difficult to compare the addresses in the tables with AM> addresses calculated by sequentially iterating over lines of text AM> after it is loaded or returned to assembler. add 100 bars per line ;) AM> Increment line number AM> no need for anything - in case of an error, I’ll just save the address of the erroneous line AM> in the error buffer. sound idea. Only then will you have to recalculate, but this is of little importance. AM> There is also no need - the text in memory will be stored in the form, maximum AM> chewed for assembler. if there are many commands in a line, you need to know which one is the error. however, by memorizing the address this is solved, however, after the slightest editing of the address text will move out. AM> metka push hl:adc hl,de:ret will look like: AM> [16] [simple label] [5] "metka" [push] [hl] [] [adc] [hl,de] [] [ret] AM> ^ AM> │ AM> │ AM> │ AM> └simple label [5] - length of the mark? it hurts so much ;) DL>> in the general case - for expressions that are not currently evaluated DL>> a list is maintained containing themselves, the place where it should be DL>> the value is written, and the method for entering this value. AM> This buffer will overflow very quickly. What, do you feel sorry for your memory? ;) Best wishes. Dmitry.