Breaking Enigma with Index of Coincidence on a Commodore 64

2026/04/01 3:16

Breaking Enigma with Index of Coincidence on a Commodore 64

RSS: https://news.ycombinator.com/rss

要約

本文

Everything we’ve done so far requires a crib. The brute-force cracker tested candidate settings against known plaintext. Turing’s Bombe used contradictions between known plaintext and ciphertext to eliminate wrong settings. No crib, no attack. But what if you intercepted a message and had no idea what it said? No weather report header, no formulaic greeting, no re-sent traffic. Just ciphertext. William Friedman developed a tool for exactly this problem in 1922. It doesn’t need known plaintext. It doesn’t even need to know the language. It measures whether a piece of text looks like language at all or whether it looks like random noise. He called it the index of coincidence. What the Index of Coincidence Measures Pick two random letters from a piece of English text. What’s the probability they’re the same letter? Not very high, but higher than you’d think, because letter frequencies aren’t uniform. E appears about 13% of the time. Z appears less than 0.1%. Two randomly chosen letters are more likely to both be E than to both be Z. For English text, the probability of a match is about 0.0667. For German it’s about the same (0.0762 by some measurements, but the exact number depends on the text). For truly random text where all 26 letters are equally likely, it’s 1/26 = 0.0385. That difference is the entire attack. Decrypt a ciphertext with the wrong Enigma settings and the output looks random: IC near 0.038. Decrypt with the right settings and the output is German: IC near 0.067. No crib needed. The statistical properties of the language itself are the signal. The Formula The IC of a text with N letters is: $$IC = \frac{\sum_{i=0}^{25} n_i \cdot (n_i - 1)}{N \cdot (N - 1)}$$where $n_i$ is how many times letter $i$ appears. The numerator counts how many ways you could pick two copies of the same letter. The denominator counts how many ways you could pick any two letters. The ratio is the probability that two randomly chosen letters match. For a C64 implementation, we don’t need the division. We just compute the numerator (call it the IC sum) and compare it to a threshold. If IC should be above 0.050, that means the sum should be above $0.050 \times N \times (N-1)$. For a 60-character message, that’s $0.050 \times 60 \times 59 = 177$. A 16-bit integer comparison. No floating point needed. The C64’s BASIC ROM has a full floating-point engine, but we don’t have to touch it. Why the Plugboard Doesn’t Matter This is the best part. The plugboard is a monoalphabetic substitution: it swaps pairs of letters before and after the rotors. If R and M are swapped, every R in the output becomes M and every M becomes R. But the total count of each pair stays the same. The letter that appeared 8 times still appears 8 times, it just has a different name. The IC doesn’t care about which letters are frequent. It only cares about how frequent the most frequent letters are. A text where E appears 13% of the time has the same IC as a text where Q appears 13% of the time. The plugboard rearranges the labels on the frequency bins without changing the bin sizes. This means IC attacks only the rotor settings (5.9 million candidates, or 103.9 billion with ring settings). Once you’ve found the right rotors and positions, you can solve the plugboard separately using frequency analysis. A Bombe solves both at once. IC splits them into two smaller problems. The Scenario In the brute-force cracker, we intercepted a U-boat weather report from the Bay of Biscay and used “WETTER” as a crib to find the settings. This time, we have a longer intercept from the same transmission, but we don’t know it’s a weather report. We don’t know anything about the plaintext. All we have is 60 characters of ciphertext: YDMAOIGMPQZPFVRCIGIIKJVECBDNPDITBYRYNKOCNJHIIVWXYUJBCDYGKVHW The correct settings are still rotors III-I-V at positions M-C-Q. We need to find them using nothing but the statistical fingerprint of the German language. How Well Does It Work? Before writing code, let’s check the numbers. Decrypt the ciphertext with all 17,576 positions for the correct rotor ordering (III-I-V) and compute the IC of each result. The correct decryption (M-C-Q) has an IC of 0.0729 and an IC sum of 258. The text reads WETTERVORHERSAGEBISKAYAHEUTEREGENMITWINDSTAERKEFUENFAUSOSTEN. “Weather forecast Biscay, today rain with wind strength five from east.” A routine U-boat weather report. At different thresholds, here’s how many of the 17,576 positions survive for a single rotor ordering (III-I-V):

      Threshold
      IC sum
      Candidates
      Percent
  


  
      0.040
      141
      6,233
      35.5%
  
  
      0.045
      159
      1,540
      8.8%
  
  
      0.050
      177
      286
      1.6%
  
  
      0.055
      194
      49
      0.3%
  
  
      0.060
      212
      7
      0.04%
  

The BASIC version searches one ordering at a time, so a threshold of 177 (IC >= 0.050) is reasonable at 286 candidates per ordering. The assembly version searches all 336 orderings, so it uses a tighter threshold of 194 (IC >= 0.055) to keep the output manageable. At 194, roughly 49 candidates survive per ordering, about 18,165 total across all 336. Even with that filtering, you can’t just sort by IC and pick the winner. The correct answer ranks #42 out of those 18,165 candidates. The top 10 are all gibberish with repeated letters that inflate the score.

      #
      IC Sum
      Rotors
      Position
      First 40 characters
  


  
      1
      288
      I-II-VII
      K-Q-Q
      VSIGDAWELGIIOEIFLHNOAFIIDPIKISGIAIGQWAGFS
  
  
      2
      284
      VI-VII-I
      V-Y-J
      NJYOJQAWZJUJJZKJAZMFZZQVJRJDAJPGOABJFIVQU
  
  
      3
      280
      VIII-I-V
      Z-O-X
      QBXRKCVQBXGRLAGLBDDUBVFBDSKXVPQYYMWBBBBBB
  
  
      4
      278
      VI-II-VIII
      M-U-N
      VMHHHMHPMHAEHCIBHHLPHLZHHIAQSUZDTUICHQISY
  
  
      5
      278
      II-V-VII
      L-B-M
      DJTKKHTUUXHHIOOJTOOBYHTJTTMMAMOJYEWJAATFM
  
  
      …
      
      
      
      
  
  
      42
      258
      III-I-V
      M-C-Q
      WETTERVORHERSAGEBISKAYAHEUTEREGENMITWIND
  



  The correct answer, III-I-V at M-C-Q, among the gibberish. IC sum 258, ranked #42 overall.

With only 60 characters, random letter distributions can outscore real language. The correct answer stands out because it’s the only one that reads like German, not because it has the highest score. You find it by reading the printer output, not by sorting a spreadsheet. Longer messages stabilize the IC. With 200+ characters, the correct decryption reliably scores highest. With 60, you need a threshold that keeps a manageable number of candidates for human review. Pseudocode for each rotor ordering (336 permutations): for each starting position (17,576 per ordering): decrypt the entire ciphertext with this setting count letter frequencies (26 bins) compute IC sum = sum of n_i * (n_i - 1) if IC sum >= threshold: CANDIDATE FOUND, print settings and decryption The per-candidate cost is higher than the brute-force cracker. That approach tested the first character of the crib and bailed immediately on mismatch, averaging about one encryption per candidate. IC requires encrypting the entire message: 60 encryptions for our 60-character ciphertext, plus the frequency counting and sum computation. But there’s no crib to find. No interrogating captured submariners, no stealing codebooks. Just raw ciphertext and math. Across all 5.9 million candidates (336 orderings), a threshold of 0.050 produces about 94,887 hits (1.6%). The correct answer is among them, but so are thousands of gibberish decryptions that happen to have uneven letter distributions. A human scanning the output would spot the readable German, but an automated system would need additional filtering (bigram frequencies, dictionary checks, or a higher threshold that risks missing the answer). BASIC Version The BASIC version searches all 17,576 positions for a single rotor ordering. The rotor wiring, stepping, and encryption subroutines are identical to the cracker. The new code is the IC computation. 5 PRINT CHR$(5) 10 PRINT CHR$(147);" ENIGMA IC ATTACK" 20 PRINT " INDEX OF COINCIDENCE":PRINT 100 DIM RF(7,25),RI(7,25),RE(25) 110 FOR R=0 TO 7:FOR I=0 TO 25 120 READ RF(R,I):NEXT I,R 200 DATA 4,10,12,5,11,6,3,16,21,25 210 DATA 13,19,14,22,24,7,23,20,18,15 220 DATA 0,8,1,17,2,9 230 DATA 0,9,3,10,18,8,17,20,23,1 240 DATA 11,7,22,19,12,2,16,6,25,13 250 DATA 15,24,5,21,14,4 260 DATA 1,3,5,7,9,11,2,15,17,19 270 DATA 23,21,25,13,24,4,8,22,6,0 280 DATA 10,12,20,18,16,14 290 DATA 4,18,14,21,15,25,9,0,24,16 300 DATA 20,8,17,7,23,11,13,5,19,6 310 DATA 10,3,2,12,22,1 320 DATA 21,25,1,17,6,8,19,24,20,15 330 DATA 18,3,13,7,11,23,0,22,12,9 340 DATA 16,14,5,4,2,10 350 DATA 9,15,6,21,14,20,12,5,24,16 360 DATA 1,4,13,7,25,17,3,10,0,18 370 DATA 23,11,8,2,19,22 380 DATA 13,25,9,7,6,17,2,23,12,24 390 DATA 18,22,1,14,20,5,0,8,21,11 395 DATA 15,4,10,16,3,19 400 DATA 5,10,16,7,19,11,23,14,2,1 410 DATA 9,18,15,3,25,17,0,12,4,22 420 DATA 13,8,20,24,6,21 450 REM COMPUTE INVERSE TABLES 460 FOR R=0 TO 7:FOR I=0 TO 25 470 RI(R,RF(R,I))=I 480 NEXT I,R 500 REM REFLECTOR UKW-B 510 FOR I=0 TO 25:READ RE(I):NEXT 520 DATA 24,17,20,7,16,18,11,3,15,23 530 DATA 13,6,14,10,12,8,4,1,5,25 540 DATA 2,22,21,9,0,19 600 REM NOTCH POSITIONS (TWO PER ROTOR) 610 DIM NO(7,1) 620 NO(0,0)=16:NO(0,1)=-1 630 NO(1,0)=4:NO(1,1)=-1 640 NO(2,0)=21:NO(2,1)=-1 650 NO(3,0)=9:NO(3,1)=-1 660 NO(4,0)=25:NO(4,1)=-1 670 NO(5,0)=25:NO(5,1)=12 680 NO(6,0)=25:NO(6,1)=12 690 NO(7,0)=25:NO(7,1)=12 700 REM CIPHERTEXT (60 CHARS, 0-25) 710 DIM CT(59) 720 CT$="YDMAOIGMPQZPFVRCIGIIKJV" 725 CT$=CT$+"ECBDNPDITBYRYNKOCNJHII" 727 CT$=CT$+"VWXYUJBCDYGKVHW" 730 FOR I=0 TO 59 740 CT(I)=ASC(MID$(CT$,I+1,1))-65 750 NEXT 760 NL=60:TH=177 770 DIM FR(25) 800 PRINT "SELECT 3 ROTORS (1-8)" 810 INPUT "LEFT ROTOR";RL 820 INPUT "MIDDLE ROTOR";RM 830 INPUT "RIGHT ROTOR";RR 840 RL=RL-1:RM=RM-1:RR=RR-1 850 PRINT:PRINT "SEARCHING 17,576 POSITIONS" 860 PRINT "FOR ROTORS";RL+1;"-";RM+1;"-";RR+1 865 PRINT "THRESHOLD:";TH 870 PRINT:TI$="000000":SC=0 900 FOR L0=0 TO 25 910 PRINT CHR$(L0+65);" "; 920 FOR M0=0 TO 25:FOR R0=0 TO 25 930 GOSUB 5000 940 IF F=1 THEN GOSUB 7000 950 NEXT R0,M0,L0 960 PRINT:PRINT 970 PRINT SC;"CANDIDATES FOUND" 975 PRINT "TIME:";INT(TI/60);"SECONDS" 980 END 2000 REM STEP ROTORS 2010 IF MP<>NO(RM,0)ANDMP<>NO(RM,1) THEN 2040 2020 LP=LP+1:LP=LP-INT(LP/26)*26 2030 MP=MP+1:MP=MP-INT(MP/26)*26 2040 IF RP<>NO(RR,0)ANDRP<>NO(RR,1) THEN 2060 2050 MP=MP+1:MP=MP-INT(MP/26)*26 2060 RP=RP+1:RP=RP-INT(RP/26)*26 2070 RETURN 3000 REM ENCRYPT (FORWARD) 3010 E=(C+RP):E=E-INT(E/26)*26 3020 C=RF(RR,E) 3030 C=(C-RP+26):C=C-INT(C/26)*26 3040 E=(C+MP):E=E-INT(E/26)*26 3050 C=RF(RM,E) 3060 C=(C-MP+26):C=C-INT(C/26)*26 3070 E=(C+LP):E=E-INT(E/26)*26 3080 C=RF(RL,E) 3090 C=(C-LP+26):C=C-INT(C/26)*26 3100 REM REFLECTOR 3110 C=RE(C) 3120 REM ENCRYPT (INVERSE) 3130 E=(C+LP):E=E-INT(E/26)*26 3140 C=RI(RL,E) 3150 C=(C-LP+26):C=C-INT(C/26)*26 3160 E=(C+MP):E=E-INT(E/26)*26 3170 C=RI(RM,E) 3180 C=(C-MP+26):C=C-INT(C/26)*26 3190 E=(C+RP):E=E-INT(E/26)*26 3200 C=RI(RR,E) 3210 C=(C-RP+26):C=C-INT(C/26)26 3220 RETURN 5000 REM IC TEST 5010 LP=L0:MP=M0:RP=R0 5020 REM CLEAR FREQUENCY TABLE 5030 FOR I=0 TO 25:FR(I)=0:NEXT 5040 REM DECRYPT AND COUNT 5050 FOR I=0 TO NL-1 5060 C=CT(I):GOSUB 2000:GOSUB 3000 5070 FR(C)=FR(C)+1 5080 NEXT 5090 REM COMPUTE IC SUM 5100 S=0 5110 FOR I=0 TO 25 5120 S=S+FR(I)(FR(I)-1) 5130 NEXT 5140 IF S>=TH THEN F=1:RETURN 5150 F=0:RETURN 7000 REM SHOW CANDIDATE 7010 SC=SC+1 7020 PRINT:PRINT "CANDIDATE";SC 7030 PRINT "POSITION: ";CHR$(L0+65);"-"; 7035 PRINT CHR$(M0+65);"-";CHR$(R0+65) 7040 PRINT "IC SUM:";S 7050 LP=L0:MP=M0:RP=R0 7060 FOR I=0 TO NL-1 7070 C=CT(I):GOSUB 2000:GOSUB 3000 7080 PRINT CHR$(C+65); 7090 NEXT 7100 PRINT:RETURN The IC test at line 5000 is where the action is. It decrypts all 60 characters, counts the frequency of each letter in the FR array, then computes the IC sum: $\sum n_i \times (n_i - 1)$. If the sum meets the threshold (177, corresponding to IC >= 0.050), it’s a candidate. Line 7000 prints each candidate with its position, IC sum, and decrypted text so you can scan for readable German. The threshold of 177 comes from $0.050 \times 60 \times 59 = 177$. Lower it and you get more candidates (and more false positives). Raise it and you might miss the correct answer if the IC is noisy. For a 60-character message, 177 is a reasonable middle ground. To check that it’s working, enter rotors 3, 1, 5 (the correct ordering from the previous articles). The program searches all 17,576 positions and prints each candidate that passes the threshold. Most of the output is gibberish with IC sums in the 180-200 range. But candidate 146 jumps out: CANDIDATE 146 POSITION: M-C-Q IC SUM: 258 WETTERVORHERSAGEBISKAYAHEUTEREGENMITWINDSTAERKEFUENFAUSOSTEN That’s readable German. IC sum 258, well above the threshold of 177. Out of 286 total candidates for this rotor ordering, this is the only one that’s actual language. The rest are random letter salad that happened to have uneven frequency distributions. Try entering a wrong ordering like 1, 2, 3. You’ll still get candidates (any ordering produces a few statistical flukes), but none of them will be readable. That’s the difference: wrong settings produce noise that occasionally looks uneven, while the right settings produce text that looks like German because it is German. Assembly Version The assembly version does the full search: all 336 rotor orderings, all 17,576 positions each. The structure is identical to the cracker, with the crib test replaced by the IC computation. Here’s what the C64 has to chew through:

      Count
  


  
      Rotor orderings (P(8,3))
      336
  
  
      Positions per ordering (26³)
      17,576
  
  
      Total candidates
      5,905,536
  
  
      Encryptions per candidate
      60
  
  
      Total encryptions
      354,332,160
  
  
      Rotor passes per encryption
      6
  
  
      Total rotor passes
      2,125,992,960
  
  
      Frequency table updates
      354,332,160
  
  
      IC sum computations (26 bins each)
      5,905,536
  

Over two billion rotor passes on a 1 MHz processor. The C64’s theoretical peak is about 0.5 MIPS (all 2-cycle instructions, which never happens in practice). Real-world throughput with mixed instructions is closer to 0.3 MIPS. Over 82 hours, the 6510 executes roughly 87 billion instructions and burns through 303 billion clock cycles. For more on what the 6510 can and can’t do at 1 MHz, see How Fast Can a 6502 Transfer Memory?. The brute-force cracker averaged about one encryption per candidate thanks to early exit. The IC attack does 60 every time, no shortcuts. The IC Test The IC test decrypts the entire message, counts letter frequencies in a 26-byte table, and computes the IC sum as a 16-bit value: ; ic test ; decrypts ciphertext, computes ic sum ; returns z flag set if sum >= threshold testic ; clear frequency table ldx #25 lda #0 ticlr sta freq,x dex bpl ticlr

      ; set starting positions
      lda trylp
      sta leftpos
      lda trymp
      sta midpos
      lda tryrp
      sta rightpos

      ; decrypt and count
      ldx #0

tidec lda cipher,x stx savex jsr encrypt ; increment freq[a] tax inc freq,x ldx savex inx cpx #60 bne tidec

      ; compute ic sum (16-bit)
      ; sum = sigma(freq[i]*(freq[i]-1))
      lda #0
      sta iclo
      sta ichi
      ldx #0

tisum lda freq,x beq tiskip ; a = n = freq[i] ; compute n*(n-1) ; freq values max ~10 for 60 chars ; so product fits in one byte tay dey sty temp iny lda #0 clc timul adc temp dey bne timul ; add to 16-bit sum clc adc iclo sta iclo bcc tiskip inc ichi tiskip inx cpx #26 bne tisum

      ; compare sum to threshold (194)
      ; 16-bit compare: ichi:iclo >= 0:194
      lda ichi
      bne tipass
      lda iclo
      cmp #194
      bcs tipass
      lda #1
      rts

tipass lda #0 rts The frequency counting is simple: decrypt each character with jsr encrypt, use the result as an index into freq, and increment. After all 60 characters, loop through the 26 frequency bins computing $n \times (n-1)$ for each. The maximum possible IC sum (if all 60 characters were the same letter) would be $60 \times 59 = 3540$, which fits in 16 bits. We accumulate the total in iclo/ichi. The multiplication uses repeated addition. With frequency values rarely above 10 for a 60-character message, this loop runs at most 10 iterations per bin. For bins with count 0 or 1, the product is zero and we skip the multiply entirely. The threshold compare is a 16-bit check. If the high byte is nonzero, the sum is at least 256, which is above 194, so it passes immediately. Otherwise compare the low byte to 194. The assembly version uses a tighter threshold (194, IC >= 0.055) than the BASIC version (177, IC >= 0.050). The BASIC version searches one rotor ordering at a time, so 286 candidates is manageable. The assembly version searches all 336 orderings, and at 177 that would produce roughly 95,000 candidates. At 194, it drops to about 16,500. The correct answer scores 258, so it passes either threshold easily. The Search Loop The position loop calls testic instead of the loop test and crib verification. There’s no two-phase filtering here, just one test: sprp ; test ic jsr testic beq tifound

      ; failed
      inc tryrp
      lda tryrp
      cmp #26
      bne sprp
      inc trymp
      lda trymp
      cmp #26
      bne spmp
      inc trylp
      lda trylp
      cmp #26
      bne splp
      rts

tifound ; print candidate jsr showcand ; keep searching (don't stop) jmp tinext One difference from the cracker: we don’t stop at the first match. The IC test produces multiple candidates, so we print each one and keep searching. The operator reads the output and picks the one that looks like German. In practice, you’d want a printer connected. The search runs for hours and produces thousands of candidates. They scroll off a 25-line screen long before the search finishes. With a printer, you get a paper log to read through when it’s done. open 4,4:cmd 4 before running redirects output to a Commodore printer. print#4:close 4 when finished. Showing Candidates When a candidate passes the IC test, we print the rotor ordering, position, IC sum, and the first 40 characters of the decrypted text. The operator scans for readable German among the gibberish: showcand lda #13 jsr chrout ; print position lda trylp clc adc #65 jsr chrout lda #45 jsr chrout lda trymp clc adc #65 jsr chrout lda #45 jsr chrout lda tryrp clc adc #65 jsr chrout lda #32 jsr chrout ; print ic sum lda ichi ldx iclo jsr $bdcd lda #32 jsr chrout ; decrypt and print first 40 chars lda trylp sta leftpos lda trymp sta midpos lda tryrp sta rightpos ldx #0 scloop lda cipher,x stx savex jsr encrypt clc adc #65 jsr chrout ldx savex inx cpx #40 bne scloop rts The Full Listing Here’s the complete IC attack. It assembles at $c000 and runs with sys 49152. The rotor wiring, stepping, encryption, and search structure are identical to the cracker. The only new code is testic and showcand. The border color changes with each new rotor ordering, so if it’s still cycling, it’s still searching. Same advice about the printer applies here. The assembly version searches all 336 orderings and produces thousands of candidates over several hours. Hook up a printer before running, or you’ll only see whatever’s still on screen when it finishes: OPEN 4,4:CMD 4:SYS 49152:PRINT#4:CLOSE 4 ; ============================== ; enigma ic attack ; index of coincidence search ; commodore 64 ; rotors i-viii (m3), no plugboard ; turbo macro pro / tmpx ; by michael doornbos 2026 ; mike@imapenguin.com ; ; decrypts ciphertext with each ; candidate setting, computes ic ; of the result, flags candidates ; with ic above threshold ; ; no crib needed ; ; .null/.text = screen codes ; use .byte for petscii strings ; ==============================

  • = $c000

; --- zero page --- ptr = $50 rightpos = $fb midpos = $fc leftpos = $fd temp = $fe

chrout = $ffd2

; === entry point === ; zero jiffy clock and accumulator lda #0 sta $a2 sta $a1 sta $a0 sta jtotal sta jtotal+1 sta jtotal+2 sta jtotal+3

      ; cls, white text
      lda #$93
      jsr chrout
      lda #5
      jsr chrout
      lda #13
      jsr chrout

      ; header
      ldx #<stitle
      ldy #>stitle
      jsr print
      lda #13
      jsr chrout
      ldx #<ssub
      ldy #>ssub
      jsr print
      lda #13
      jsr chrout
      lda #13
      jsr chrout

      ; searching message
      ldx #<ssearch
      ldy #>ssearch
      jsr print
      lda #13
      jsr chrout

      ; init candidate count
      lda #0
      sta candlo
      sta candhi

      ; run search
      jsr searchall

      ; restore border
      lda #14
      sta $d020

      ; show total
      lda #13
      jsr chrout
      lda #13
      jsr chrout
      ldx #<stotal
      ldy #>stotal
      jsr print
      lda candhi
      ldx candlo
      jsr $bdcd
      ldx #<scands
      ldy #>scands
      jsr print
      lda #13
      jsr chrout

      ; show elapsed time
      ldx #<stime
      ldy #>stime
      jsr print
      jsr jiffysec
      lda #13
      jsr chrout
      rts

; === search all orderings === ; p(8,3) = 336 permutations searchall lda #0 sta tryl saleft lda #0 sta trym samid lda trym cmp tryl beq sasm lda #0 sta tryr saright lda tryr cmp tryl beq sasr cmp trym beq sasr ; valid permutation jsr searchpos sasr inc tryr lda tryr cmp #8 bne saright sasm inc trym lda trym cmp #8 bne samid inc tryl lda tryl cmp #8 bne saleft sadone rts

; === search positions === ; 26^3 = 17,576 per ordering searchpos ; accumulate jiffy clock, then zero it ; prevents kernal midnight reset (24h) sei clc lda jtotal adc $a2 sta jtotal lda jtotal+1 adc $a1 sta jtotal+1 lda jtotal+2 adc $a0 sta jtotal+2 bcc jnoc inc jtotal+3 jnoc lda #0 sta $a2 sta $a1 sta $a0 cli ; configure rotors lda tryl sta leftsel lda trym sta midsel lda tryr sta rightsel ; border color = progress inc $d020 ; sweep all positions lda #0 sta trylp splp lda #0 sta trymp spmp lda #0 sta tryrp sprp ; test ic jsr testic beq tifound ; failed jmp tinext tifound ; show candidate jsr showcand tinext inc tryrp lda tryrp cmp #26 bne sprp inc trymp lda trymp cmp #26 bne spmp inc trylp lda trylp cmp #26 bne splp rts

; === test ic === ; decrypts ciphertext ; computes ic sum ; returns z flag set if >= threshold testic ; clear frequency table ldx #25 lda #0 ticlr sta freq,x dex bpl ticlr

      ; set starting positions
      lda trylp
      sta leftpos
      lda trymp
      sta midpos
      lda tryrp
      sta rightpos

      ; decrypt all 60 chars
      ldx #0

tidec lda cipher,x stx savex jsr encrypt ; count frequency tax inc freq,x ldx savex inx cpx #60 bne tidec

      ; compute ic sum (16-bit)
      lda #0
      sta iclo
      sta ichi
      ldx #0

tisum lda freq,x beq tiskp cmp #1 beq tiskp ; a = n, compute n*(n-1) tay dey sty temp iny lda #0 clc timul adc temp dey bne timul ; add to 16-bit sum clc adc iclo sta iclo bcc tiskp inc ichi tiskp inx cpx #26 bne tisum

      ; compare to threshold 194
      lda ichi
      bne tiyes
      lda iclo
      cmp #194
      bcs tiyes
      ; below threshold
      lda #1
      rts

tiyes lda #0 rts

; === show candidate === showcand ; increment count inc candlo bne scnoc inc candhi scnoc lda #13 jsr chrout ; print ordering ldx leftsel jsr printrn lda #45 jsr chrout ldx midsel jsr printrn lda #45 jsr chrout ldx rightsel jsr printrn lda #32 jsr chrout ; print position lda trylp clc adc #65 jsr chrout lda #45 jsr chrout lda trymp clc adc #65 jsr chrout lda #45 jsr chrout lda tryrp clc adc #65 jsr chrout lda #32 jsr chrout ; print ic sum lda ichi ldx iclo jsr $bdcd lda #32 jsr chrout ; decrypt first 40 chars lda trylp sta leftpos lda trymp sta midpos lda tryrp sta rightpos ldx #0 scloop lda cipher,x stx savex jsr encrypt clc adc #65 jsr chrout ldx savex inx cpx #40 bne scloop rts

; === encrypt === ; a = letter (0-25) in/out ; no plugboard (identity) encrypt pha jsr step pla ; right fwd ldx rightsel jsr setfwd ldx rightpos jsr rotorpass ; middle fwd ldx midsel jsr setfwd ldx midpos jsr rotorpass ; left fwd ldx leftsel jsr setfwd ldx leftpos jsr rotorpass ; reflector tax lda reflector,x ; left inv ldx leftsel jsr setinv ldx leftpos jsr rotorpass ; middle inv ldx midsel jsr setinv ldx midpos jsr rotorpass ; right inv ldx rightsel jsr setinv ldx rightpos jsr rotorpass rts

; === step rotors === ; dual notch for vi-viii step ldx midsel lda midpos cmp notch1,x beq dodouble cmp notch2,x bne nodouble dodouble ; double step lda leftpos clc adc #1 jsr mod26 sta leftpos lda midpos clc adc #1 jsr mod26 sta midpos jmp stepright nodouble ldx rightsel lda rightpos cmp notch1,x beq domid cmp notch2,x bne stepright domid lda midpos clc adc #1 jsr mod26 sta midpos stepright lda rightpos clc adc #1 jsr mod26 sta rightpos rts

; === mod26 === mod26 cmp #26 bcc m26done sbc #26 m26done rts

; === rotor pass === rotorpass stx temp clc adc temp jsr mod26 tay lda (ptr),y sec sbc temp clc adc #26 jsr mod26 rts

; === set table pointer === setfwd ldy fwdlo,x sty ptr ldy fwdhi,x sty ptr+1 rts

setinv ldy invlo,x sty ptr ldy invhi,x sty ptr+1 rts

; === print string === ; x=lo, y=hi, null-terminated print stx ptr sty ptr+1 ldy #0 ploop lda (ptr),y beq pdone jsr chrout iny bne ploop pdone rts

; === print rotor name === ; x = rotor index (0-7) printrn lda rnlo,x sta ptr lda rnhi,x sta ptr+1 ldy #0 rnloop lda (ptr),y beq rndone jsr chrout iny bne rnloop rndone rts

; === jiffy clock to hours/minutes === ; adds remaining jiffies to 32-bit total ; divides by 60 three times: jiffies->sec->min->hr jiffysec ; add remaining jiffies to accumulator sei clc lda jtotal adc $a2 sta dividend+3 lda jtotal+1 adc $a1 sta dividend+2 lda jtotal+2 adc $a0 sta dividend+1 lda jtotal+3 adc #0 sta dividend cli ; 32-bit divide by 60 lda #0 sta remainder ldx #32 jsloop asl dividend+3 rol dividend+2 rol dividend+1 rol dividend rol remainder lda remainder cmp #60 bcc jsskip sbc #60 sta remainder inc dividend+3 jsskip dex bne jsloop ; dividend = total seconds ; divide by 60 to get minutes lda dividend sta jsecs lda dividend+1 sta jsecs+1 lda dividend+2 sta jsecs+2 lda dividend+3 sta jsecs+3 lda #0 sta remainder ldx #32 jsloop2 asl jsecs+3 rol jsecs+2 rol jsecs+1 rol jsecs rol remainder lda remainder cmp #60 bcc jsskp2 sbc #60 sta remainder inc jsecs+3 jsskp2 dex bne jsloop2 ; divide minutes by 60 to get hours lda #0 sta remainder ldx #32 jsloop3 asl jsecs+3 rol jsecs+2 rol jsecs+1 rol jsecs rol remainder lda remainder cmp #60 bcc jsskp3 sbc #60 sta remainder inc jsecs+3 jsskp3 dex bne jsloop3 ; print hours lda jsecs+2 ldx jsecs+3 jsr $bdcd lda #72 ; 'h' jsr chrout lda #32 jsr chrout ; print remaining minutes lda #0 ldx remainder jsr $bdcd lda #77 ; 'm' jsr chrout rts

dividend .byte 0,0,0,0 jsecs .byte 0,0,0,0 remainder .byte 0

; === data ===

; search state savex .byte 0 jtotal .byte 0,0,0,0 tryl .byte 0 trym .byte 0 tryr .byte 0 trylp .byte 0 trymp .byte 0 tryrp .byte 0

; rotor config rightsel .byte 0 midsel .byte 0 leftsel .byte 0

; ic state iclo .byte 0 ichi .byte 0 candlo .byte 0 candhi .byte 0

; frequency table (26 bytes) freq .repeat 26,0

; ciphertext (60 chars, 0-25) cipher .byte 24,3,12,0,14,8 .byte 6,12,15,16,25,15 .byte 5,21,17,2,8,6 .byte 8,8,10,9,21,4 .byte 2,1,3,13,15,3 .byte 8,19,1,24,17,24 .byte 13,10,14,2,13,9 .byte 7,8,8,21,22,23 .byte 24,20,9,1,2,3 .byte 24,6,10,21,7,22

; notch positions (i-viii) notch1 .byte 16,4,21,9,25 .byte 25,25,25 ; second notch ($ff = none) notch2 .byte $ff,$ff,$ff,$ff,$ff .byte 12,12,12

; address tables (rotors i-viii) fwdlo .byte <rot1f,<rot2f .byte <rot3f,<rot4f .byte <rot5f,<rot6f .byte <rot7f,<rot8f fwdhi .byte >rot1f,>rot2f .byte >rot3f,>rot4f .byte >rot5f,>rot6f .byte >rot7f,>rot8f invlo .byte <rot1i,<rot2i .byte <rot3i,<rot4i .byte <rot5i,<rot6i .byte <rot7i,<rot8i invhi .byte >rot1i,>rot2i .byte >rot3i,>rot4i .byte >rot5i,>rot6i .byte >rot7i,>rot8i

; === rotor wiring ===

; i: ekmflgdqvzntowyhxuspaibrcj rot1f .byte 4,10,12,5,11,6 .byte 3,16,21,25,13,19 .byte 14,22,24,7,23,20 .byte 18,15,0,8,1,17 .byte 2,9

; ii: ajdksiruxblhwtmcqgznpyfvoe rot2f .byte 0,9,3,10,18,8 .byte 17,20,23,1,11,7 .byte 22,19,12,2,16,6 .byte 25,13,15,24,5,21 .byte 14,4

; iii: bdfhjlcprtxvznyeiwgakmusqo rot3f .byte 1,3,5,7,9,11 .byte 2,15,17,19,23,21 .byte 25,13,24,4,8,22 .byte 6,0,10,12,20,18 .byte 16,14

; iv: esovpzjayquirhxlnftgkdcmwb rot4f .byte 4,18,14,21,15,25 .byte 9,0,24,16,20,8 .byte 17,7,23,11,13,5 .byte 19,6,10,3,2,12 .byte 22,1

; v: vzbrgityupsdnhlxawmjqofeck rot5f .byte 21,25,1,17,6,8 .byte 19,24,20,15,18,3 .byte 13,7,11,23,0,22 .byte 12,9,16,14,5,4 .byte 2,10

; vi: jpgvoumfyqbenhzrdkasxlictw rot6f .byte 9,15,6,21,14,20 .byte 12,5,24,16,1,4 .byte 13,7,25,17,3,10 .byte 0,18,23,11,8,2 .byte 19,22

; vii: nzjhgrcxmyswboufaivlpekqdt rot7f .byte 13,25,9,7,6,17 .byte 2,23,12,24,18,22 .byte 1,14,20,5,0,8 .byte 21,11,15,4,10,16 .byte 3,19

; viii: fkqhtlxocbjspdzramewniuygv rot8f .byte 5,10,16,7,19,11 .byte 23,14,2,1,9,18 .byte 15,3,25,17,0,12 .byte 4,22,13,8,20,24 .byte 6,21

; inverse tables

rot1i .byte 20,22,24,6,0,3 .byte 5,15,21,25,1,4 .byte 2,10,12,19,7,23 .byte 18,11,17,8,13,16 .byte 14,9

rot2i .byte 0,9,15,2,25,22 .byte 17,11,5,1,3,10 .byte 14,19,24,20,16,6 .byte 4,13,7,23,12,8 .byte 21,18

rot3i .byte 19,0,6,1,15,2 .byte 18,3,16,4,20,5 .byte 21,13,25,7,24,8 .byte 23,9,22,11,17,10 .byte 14,12

rot4i .byte 7,25,22,21,0,17 .byte 19,13,11,6,20,15 .byte 23,16,2,4,9,12 .byte 1,18,10,3,24,14 .byte 8,5

rot5i .byte 16,2,24,11,23,22 .byte 4,13,5,19,25,14 .byte 18,12,21,9,20,3 .byte 10,6,8,0,17,15 .byte 7,1

rot6i .byte 18,10,23,16,11,7 .byte 2,13,22,0,17,21 .byte 6,12,4,1,9,15 .byte 19,24,5,3,25,20 .byte 8,14

rot7i .byte 16,12,6,24,21,15 .byte 4,3,17,2,22,19 .byte 8,0,13,20,23,5 .byte 10,25,14,18,11,7 .byte 9,1

rot8i .byte 16,9,8,13,18,0 .byte 24,3,21,10,1,5 .byte 17,20,7,12,2,15 .byte 11,4,22,25,19,6 .byte 23,14

; ukw-b reflector reflector .byte 24,17,20,7,16,18 .byte 11,3,15,23,13,6 .byte 14,10,12,8,4,1 .byte 5,25,2,22,21,9 .byte 0,19

; === strings (petscii) ===

; " enigma ic attack" stitle .byte 32,32,69,78,73,71 .byte 77,65,32,73,67,32 .byte 65,84,84,65,67,75 .byte 0 ; " no crib needed" ssub .byte 32,32,78,79,32,67 .byte 82,73,66,32,78,69 .byte 69,68,69,68,0 ; " searching..." ssearch .byte 32,32,83,69,65,82 .byte 67,72,73,78,71,46 .byte 46,46,0 ; " total: " stotal .byte 32,32,84,79,84,65 .byte 76,58,32,0 ; " candidates" scands .byte 32,67,65,78,68,73 .byte 68,65,84,69,83,0 ; " time: " stime .byte 32,32,84,73,77,69 .byte 58,32,0 ; " seconds" ssec .byte 32,83,69,67,79,78 .byte 68,83,0

; rotor names (petscii) rn1 .byte 73,0 rn2 .byte 73,73,0 rn3 .byte 73,73,73,0 rn4 .byte 73,86,0 rn5 .byte 86,0 rn6 .byte 86,73,0 rn7 .byte 86,73,73,0 rn8 .byte 86,73,73,73,0 rnlo .byte <rn1,<rn2,<rn3 .byte <rn4,<rn5,<rn6 .byte <rn7,<rn8 rnhi .byte >rn1,>rn2,>rn3 .byte >rn4,>rn5,>rn6 .byte >rn7,>rn8 Timing

  18,165 candidates in 82 hours. The 6510 earned its keep.

The IC attack is slower per candidate than the brute-force cracker. Each candidate requires decrypting all 60 characters (60 encrypt calls) plus the frequency counting and IC sum computation. The cracker averaged about 1.04 encryptions per candidate thanks to early exit on the first crib character. On a stock NTSC C64, the full IC search through all 5.9 million candidates takes 82 hours (about 3.4 days), producing 18,165 candidates at the 194 threshold. The Jiffy Clock Problem The C64’s jiffy clock at $a0-$a2 is 24 bits, which can count up to 16,777,216 jiffies (about 77.7 hours at 60 Hz). But the KERNAL never lets it get that high. The UDTIM routine at $f69b, called 60 times per second by the IRQ handler, resets the clock to zero when it reaches 5,184,000 jiffies (24 hours). It’s a time-of-day clock, not a free-running counter. From the original Commodore source: ; here we check for roll-over 23:59:59 ; and reset the clock to zero if true ud30 sec lda time+2 sbc #$01 lda time+1 sbc #$1a lda time sbc #$4f bcc ud60 ; time has rolled--zero register stx time stx time+1 stx time+2 An 82-hour search hits this reset three times. If you just read the clock at the end, you get the time since the last midnight reset, not the total elapsed time. I learned this the hard way after a four-day run reported ten hours. The fix: at each rotor ordering (every ~15 minutes), read the jiffy clock, add it to a 32-bit accumulator, and zero the clock. The KERNAL reset never fires because the clock never reaches 24 hours. You don’t have to wait for the full search, though. The correct answer (III-I-V at M-C-Q) is ordering #87 out of 336. It shows up on the printer about 21 hours in, less than a day. The remaining 61 hours just confirm there’s nothing better hiding in the other orderings. Where the Cycles Go The rotorpass routine is where the CPU spends most of its time. It runs 2.1 billion times across the full search. Here’s every instruction with its cycle count: rotorpass stx temp ; 3 save position offset clc ; 2 adc temp ; 3 add position to letter jsr mod26 ; 17 reduce mod 26 tay ; 2 use as table index lda (ptr),y ; 5 look up rotor wiring sec ; 2 sbc temp ; 3 subtract position clc ; 2 adc #26 ; 2 keep positive jsr mod26 ; 17 reduce mod 26 rts ; 6 ; total: 64 cycles Each jsr mod26 costs 6 cycles for the call, then a compare, a conditional subtract, and a return. About 17 cycles total depending on whether the value needs reducing. Each encrypt call does 6 of these (3 forward through the rotors, 3 inverse), plus table pointer setup (setfwd/setinv at 26 cycles each), stepping, and the reflector lookup. One full encrypt comes to about 720 cycles. The inner loop does that 60 times per candidate (once per ciphertext character), then counts frequencies and computes the IC sum. Across 5.9 million candidates on an NTSC C64 at 1,022,727 Hz, the whole search takes 82 hours. The tradeoff: the IC attack trades speed for generality. It’s slower than a crib-based attack, but it doesn’t need known plaintext. When you have a crib, use it. When you don’t, IC is your best option. IC vs. Crib Attack

      Crib Attack
      IC Attack
  


  
      Needs known plaintext?
      Yes
      No
  
  
      Per-candidate cost
      ~1 encryption (early exit)
      60 encryptions + frequency counting
  
  
      Plugboard
      Ignored (solved separately)
      Ignored (IC is plugboard-invariant)
  
  
      False positives
      None (exact match)
      18,165 candidates at threshold 194
  
  
      Output
      Single correct answer
      Multiple candidates for human review
  
  
      Time (assembly, all 336 orderings)
      ~22 minutes
      ~82 hours (~3.4 days)
  

The IC attack was never used operationally against Enigma during the war. Bletchley Park had enough cribs from weather reports, re-sends, and formulaic messages that the Bombe was the practical choice. IC analysis was well understood (Friedman published it in 1922), but the computational cost of decrypting entire messages for every candidate setting made it impractical with 1940s technology. On a C64, the cost is steep but possible. With a crib, the brute-force cracker solved the same message in about 22 minutes. Without one, the IC attack takes three and a half days. That’s the price of not knowing anything about the plaintext. But the answer lands on the printer in about 21 hours. Just plug in a printer, type sys 49152, and go live your life for a while. The Same Search on Modern Hardware The IC attack is embarrassingly parallel. Every candidate is independent. No shared state, no ordering dependencies. A modern machine can throw cores and GPU threads at it without coordination. I ported the same algorithm to C and ran it three ways on an Apple M4:

      Version
      Time
      Candidates/sec
      Speedup vs C64
  


  
      C64 (6502 assembly, 1 MHz)
      82 hours
      ~20
      1x
  
  
      C, single-threaded (-O3)
      1.856s
      3.2 million
      159,000x
  
  
      C, OpenMP (all CPU cores)
      0.409s
      14.4 million
      722,000x
  
  
      Metal GPU (M4)
      0.039s
      151 million
      7,569,000x
  

All four produce the same 18,165 candidates. Same algorithm, same rotor tables, same threshold. The GPU version finishes before your finger leaves the Enter key. OpenMP splits the 336 orderings across all CPU cores. Nearly linear scaling. The GPU goes further, running all 5.9 million candidates as parallel threads. Each GPU core handles one candidate start to finish. The rotor tables and ciphertext fit in shared memory, so there’s no memory bottleneck. To put the C64’s 82 hours in perspective, here’s what the same search would take on machines available in 1983, alongside some modern hardware. These estimates scale linearly by MIPS, which is rough but directionally right for this kind of integer-heavy table-lookup workload.

      Machine
      Year
      MIPS
      Estimated time
  


  
      Commodore 64
      1982
      0.3
      82 hours (measured)
  
  
      IBM PC XT (8088, 4.77 MHz)
      1983
      0.33
      74 hours
  
  
      VAX-11/780
      1977
      1.0
      25 hours
  
  
      IBM 3081K mainframe
      1982
      16
      93 minutes
  
  
      Cray-1 (scalar)
      1976
      80
      18 minutes
  
  
      Cray X-MP (scalar, 1 CPU)
      1982
      105
      14 minutes
  
  
      AMD Ryzen 9 9950X (1 core)
      2024
      ~28,000
      ~3.2 seconds
  
  
      Apple M4 (1 core)
      2024
      ~24,000
      1.856 seconds (measured)
  
  
      Apple M4 (all cores, OpenMP)
      2024
      -
      0.409 seconds (measured)
  
  
      Apple M4 GPU (Metal)
      2024
      -
      0.039 seconds (measured)
  

The IBM PC XT is the fun one. The 8088 runs at nearly 5x the C64’s clock speed but delivers about the same integer throughput. Its 8-bit external bus and microcode overhead eat up the clock advantage. The VAX-11/780, the machine that literally defined “1 MIPS,” would have finished in about a day. The Cray-1 could have done it during a coffee break, but using a 10-million-dollar vector supercomputer for integer table lookups would have been a hard budget request to justify. Then again, if you’re trying to win a war, that budget gets approved. The M4’s actual measured time (1.856 seconds single-threaded) is faster than the MIPS estimate because MIPS doesn’t capture what modern hardware really does. The C compiler eliminates subroutine call overhead, the branch predictor nails the inner loop, and every rotor table lookup hits L1 cache. The 6510 spends 64 cycles per rotor pass with two subroutine calls. The M4 burns through the same logic in a few nanoseconds. The same math that took 82 hours on a C64 runs in 39 milliseconds on a GPU you can buy at the Apple Store.

A few ways to take this further:

Pre-filter with IC: Use IC as a first pass to narrow down rotor orderings. For each ordering, compute IC for a sample of positions. If no position scores well, skip the ordering entirely. This reduces the search space for any follow-up attack.

Bigram scoring: IC uses single-letter frequencies. Bigram frequencies (pairs like TH, ER, EN in German) are an even stronger signal. Count all 676 bigram frequencies and compare to expected German bigrams. Higher accuracy, but the frequency table grows from 26 bytes to 676 bytes.

Solve the plugboard: Once IC identifies the correct rotor settings, the plugboard can be deduced using frequency analysis. Compare the letter frequencies of the rotor-only decryption to expected German frequencies. The mapping from observed frequencies to expected frequencies reveals the plugboard swaps.

Variable threshold: Use a sliding threshold based on message length. Shorter messages need a lower threshold (more false positives) because the IC is noisier. Longer messages allow a higher threshold (fewer false positives). Pre-compute the threshold for the specific message length.

All the code from this article (6502 assembly, C, OpenMP, Metal GPU, and Python verification) is on GitHub: mrdoornbos/enigma_ic. Going Faster on the C64 The emulator article pointed out that mod26 could be inlined to save cycles. In the IC attack, rotorpass calls jsr mod26 twice per pass. Each jsr/rts pair costs 12 cycles. Over 2.1 billion rotor passes, that’s about 50 billion wasted cycles just bouncing in and out of a 3-instruction subroutine. The fix is simple. Replace the two jsr mod26 calls with the comparison inline: rotorpass stx temp clc adc temp cmp #26 bcc rp1 sbc #26 rp1 tay lda (ptr),y sec sbc temp clc adc #26 cmp #26 bcc rp2 sbc #26 rp2 rts Same logic, no subroutine calls. The mod26 routine still exists for the stepping code (which runs once per character, not worth optimizing), but the rotor pass is where the CPU lives for 82 hours.

  67 hours 21 minutes. Same 18,165 candidates, 15 hours faster.

The result: 67 hours, down from 82. An 18% speedup from removing two jsr instructions. The code is 6 bytes larger. On a machine that runs for days, inlining a hot subroutine is the easiest optimization you can make.

 Code



 How-To



 Retro



 Programming



 Tutorial



 Enigma



 Cryptography



 C64

同じ日のほかのニュース

一覧に戻る →

2026/04/05 4:39

Microsoft は現在、**「Copilot」という名称を冠した主力製品が4つあります。** | # | 製品 | 主な機能 | |---|------|-----------| | 1 | **GitHub Copilot** | Visual Studio Code、GitHub.com、その他の IDE で開発者向けに AI がコード補完を行うサービス。 | | 2 | **Microsoft 365 Copilot** | Word・Excel・PowerPoint・Outlook・Teams 等の Office アプリ内で、メール作成や文書作成、プレゼンテーション制作、データ分析などを支援する統合アシスタント。 | | 3 | **Power Platform Copilot** | Power Apps、Power Automate、Power Virtual Agents の AI 強化機能。自然言語で入力した内容をアプリロジックやフロー、チャットボットへと変換します。 | | 4 | **Dynamics 365 Copilot** | Dynamics 365 スイート全体(営業・顧客サービス・財務・運営など)において、文脈に応じた AI アシスタントが機能を補完します。 | Azure 内の「Copilot」ツール(例:Azure OpenAI Studio など)やその他ニッチな製品もありますが、上記4つが Copilot ブランドを掲げる主要商用製品です。

## Japanese Translation: **要約:** マイクロソフトは、現在「Copilot」という名称を自社エコシステム内の少なくとも75個の異なる製品・サービス・機能に使用しています。著者は、単一の情報源がすべての事例を列挙していないため、製品ページ、ローンチアナウンス、およびマーケティング資料を精査しながらこのリストを作成しました。この「Copilot」は、アプリや組み込みAIヘルパーからキーボードキー、ノートパソコンライン全体、さらには開発者が独自のCopilotを作成できるツールまで多岐にわたり、マイクロソフト製品群内でブランドがどれほど浸透しているかを示しています。読者がこの複雑な環境をナビゲートしやすくするため、各Copilotをカテゴリ別にグループ化し、それらの間のリンクを表示した視覚的マップを作成しました。このインタラクティブ図は、個々の項目をクリックして相互関係を見ることができるため、マイクロソフトの戦略を明確にし、企業や開発者にとって統合機会を浮き彫りにする可能性があります。

2026/04/04 19:26

**「極めてシンプルな自己蒸留でコード生成性能を向上させる」**

## Japanese Translation: (欠落していた詳細を追加したもの)** Self‑distillation(SSD)は、外部検証や強化学習を用いずに、自身のサンプリング出力で微調整することでLLMのコード生成精度を向上させる軽量手法です。Qwen3‑30B‑Instruct に適用すると、SSDは LiveCodeBench v6 の pass@1 を 42.4 % から 55.3 % に引き上げ、特に難易度の高いコーディングタスクで最大の改善を示しました。Qwen と Llama モデル(サイズ 4B、8B、30B)のインストラクションスタイルとシンキングスタイル両方で同様の向上が観測されました。技術は温度0.9で解答をサンプリングし、最初の512トークンで切り捨てた後、そのサンプルに対して標準的な教師付き微調整を行います。SSD の効果は、文脈依存でトークン分布を再構築することで、精度と探索性の矛盾を解決できる点に起因します。高い精度が必要な際には注意喚起トレイルを抑制し、探索中には多様性を保持します。コストのかからないポストトレーニング拡張として、RLや人間による検証を回避できるSSDは、他のLLMがコード生成性能を向上させ、ソフトウェア開発ツールや教育への広範な展開を促進する魅力的な選択肢となります。

2026/04/04 23:53

**Show HN: TurboQuant‑WASM ― ブラウザ上で動く Google のベクトル量子化**

## Japanese Translation: (以下は日本語訳です) ## Summary この記事では、**botirk38/turboquant** の新しい実験的 WebAssembly ビルドを発表しています。これは npm パッケージ `turboquant-wasm` として配布されます。このビルドは Zig 0.15.2 と Bun ツールチェーンのおかげで、Web ブラウザと Node.js の両方で動作します。WASM バイナリは緩和された SIMD 命令セット(`@mulAdd FMA → f32x4.relaxed_madd`)を使用し、ベクトル化された QJL 符号のパッキング/アンパックとスケーリングを実現します。簡易 TypeScript API(`TurboQuant.init()`、`encode()`、`decode()`、`dot()`)により、開発者は高次元ベクトルを圧縮しつつ内積精度を保持できます;出力は金額値テストで検証され、オリジナルの Zig 参照と完全に一致します。 主な技術的詳細: - **実行環境要件**:Chrome 114+、Firefox 128+、Safari 18+;Node.js 20+。 - **圧縮性能**:1024 次元ベクトルで約 4.5 ビット/次元(約 6 倍圧縮)。 - **ドット積精度**:`dot()` はデコードせずにドット積を計算し、単位ベクトルで dim = 128 の場合、平均絶対誤差 < 1.0。 リリースは Google Research の TurboQuant 論文(ICLR 2026)を基盤としており、botirk38/turboquant のオリジナル Zig 実装に感謝しています。将来のアップデートではブラウザ/Node.js サポートの拡大や、より高い次元での圧縮率・誤差メトリックの改善が期待されます。Web とサーバー環境で効率的かつ忠実なベクトル圧縮を可能にすることで、`turboquant-wasm` は開発者が帯域幅とストレージ負荷を削減しながら計算精度を維持できるよう支援します。