การคูณและการหารโดยใช้โอเปอเรเตอร์กะใน C เร็วขึ้นจริงหรือ


288

การคูณและการหารสามารถทำได้โดยใช้ตัวดำเนินการบิตตัวอย่างเช่น

i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)

และอื่น ๆ

จริง ๆ แล้วมันเร็วกว่าการใช้พูด(i<<3)+(i<<1)เพื่อคูณกับ 10 กว่าการใช้i*10โดยตรงหรือไม่ มีอินพุตอะไรบ้างที่ไม่สามารถคูณหรือหารด้วยวิธีนี้ได้?


8
ที่จริงแล้วการหารราคาถูกโดยค่าคงที่อื่นที่ไม่ใช่พลังของสองเป็นไปได้ แต่เป็นซับเจ็ตต์ที่ยุ่งยากซึ่งคุณไม่ได้ทำเพื่อความยุติธรรมกับ "/ Division … / แบ่งแยก" ในคำถามของคุณ ดูตัวอย่างเช่นhackersdelight.org/divcMore.pdf (หรือรับหนังสือ "แฮกเกอร์พอใจ" ถ้าคุณทำได้)
ปาสกาล Cuoq

46
ดูเหมือนว่าบางสิ่งที่สามารถทดสอบได้ง่าย
juanchopanza

25
ตามปกติ - มันขึ้นอยู่กับ กาลครั้งหนึ่งฉันได้ลองสิ่งนี้ในแอสเซมเบลอร์ใน Intel 8088 (IBM PC / XT) ที่การคูณใช้นาฬิกาพันล้านครั้ง เลื่อนและเพิ่มการทำงานได้เร็วขึ้นมากดังนั้นจึงเป็นความคิดที่ดี อย่างไรก็ตามในขณะที่การคูณหน่วยรถบัสมีอิสระที่จะเติมคิวการเรียนการสอนและการเรียนการสอนต่อไปก็สามารถเริ่มต้นได้ทันที หลังจากชุดของการเปลี่ยนแปลงและเพิ่มคิวคำสั่งจะว่างเปล่าและ CPU จะต้องรอให้คำสั่งถัดไปดึงจากหน่วยความจำ (หนึ่งไบต์ในแต่ละครั้ง!) วัดวัดวัด!
โบเพอร์สัน

19
นอกจากนี้โปรดระวังว่าการเลื่อนขวานั้นเหมาะสมสำหรับเลขจำนวนเต็มที่ไม่ได้ลงนามเท่านั้น หากคุณมีจำนวนเต็มที่ลงนามแล้วมันไม่ได้กำหนดว่า 0 หรือบิตสูงสุดจะถูกเสริมจากด้านซ้าย (และอย่าลืมเวลาที่คนอื่น (แม้แต่ตัวคุณเอง) จะอ่านโค้ดในปีต่อ ๆ ไป!)
Kerrek SB

29
ในความเป็นจริงคอมไพเลอร์การปรับให้เหมาะสมที่ดีจะใช้การคูณและการหารด้วยกะเมื่อเร็วขึ้น
Peter G.

คำตอบ:


487

คำตอบสั้น ๆ : ไม่น่าเป็นไปได้

คำตอบยาว: คอมไพเลอร์ของคุณมีเครื่องมือเพิ่มประสิทธิภาพที่จะรู้วิธีการคูณเร็วเท่าที่สถาปัตยกรรมโปรเซสเซอร์เป้าหมายของคุณมีความสามารถ ทางออกที่ดีที่สุดของคุณคือการบอกผู้รวบรวมเจตนาของคุณอย่างชัดเจน (เช่น i * 2 มากกว่า i << 1) และปล่อยให้มันเป็นตัวตัดสินว่าแอสเซมบลี / รหัสเครื่องที่เร็วที่สุดคืออะไร มีความเป็นไปได้ที่ตัวประมวลผลจะใช้คำสั่งคูณเป็นลำดับของการเลื่อนและเพิ่มในไมโครโค้ด

บรรทัดล่างสุด - อย่าใช้เวลากังวลกับเรื่องนี้ ถ้าคุณตั้งใจจะกะ ถ้าคุณต้องการคูณให้คูณ ทำสิ่งที่ชัดเจนที่สุดทางความหมาย - เพื่อนร่วมงานของคุณจะขอบคุณในภายหลัง หรือมีแนวโน้มว่าจะสาปแช่งคุณในภายหลังหากคุณทำอย่างอื่น


31
ใช่ดังที่กล่าวไปแล้วว่าผลกำไรที่เป็นไปได้สำหรับเกือบทุกแอปพลิเคชันจะมีค่ามากกว่าความสับสนที่แนะนำทั้งหมด ไม่ต้องกังวลกับการเพิ่มประสิทธิภาพแบบนี้ก่อนเวลาอันควร สร้างสิ่งที่เป็น sematically ชัดเจนระบุคอขวดและเพิ่มประสิทธิภาพจากที่นั่น ...
เดฟ

4
การตกลงการปรับให้เหมาะสมสำหรับการอ่านและการบำรุงรักษาอาจทำให้คุณมีเวลามากขึ้นที่จะใช้เวลาในการปรับปรุงสิ่งที่profilerกล่าวว่าเป็นเส้นทางรหัสร้อน
doug65536

5
ความคิดเห็นเหล่านี้ทำให้ดูเหมือนว่าคุณกำลังละทิ้งประสิทธิภาพที่อาจเกิดขึ้นจากการบอกคอมไพเลอร์ว่าจะทำงานอย่างไร นี่ไม่ใช่กรณี คุณได้รับจริงดีกว่ารหัสจากgcc -O3บน x86 ที่มีreturn i*10กว่าจากรุ่นกะ ในฐานะคนที่ดูคอมไพเลอร์เอาท์พุทมาก (ดูคำตอบ asm / การเพิ่มประสิทธิภาพของฉัน) ฉันไม่แปลกใจ มีหลายครั้งที่มันช่วยให้คอมไพเลอร์จับมือเป็นวิธีหนึ่งในการทำสิ่งต่าง ๆแต่นี่ไม่ใช่หนึ่งในนั้น gcc เก่งคณิตศาสตร์เลขเนื่องจากมันสำคัญ
Peter Cordes

เพียงแค่ดาวน์โหลดภาพร่างอาร์ดิโนที่มีmillis() >> 2; มันจะมากเกินไปไหมที่จะขอแบ่งกัน?
พอลวีแลนด์

1
ฉันทดสอบi / 32vs i >> 5และi / 4vs i >> 2on gcc สำหรับ cortex-a9 (ซึ่งไม่มีการแบ่งฮาร์ดแวร์) ด้วยการเพิ่มประสิทธิภาพ -O3 และแอสเซมบลีที่เกิดขึ้นนั้นเหมือนกันทุกประการ ฉันไม่ชอบการใช้งานดิวิชั่นก่อน แต่มันอธิบายถึงความตั้งใจของฉันและผลลัพธ์จะเหมือนกัน
robsn

91

เพียงจุดที่เป็นรูปธรรมของการวัด: หลายปีก่อนฉันได้ทำการเปรียบเทียบอัลกอริทึมการแปลงแป้นพิมพ์ของฉันสองเวอร์ชัน:

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = 127 * h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

และ

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = (h << 7) - h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

ในทุกเครื่องที่ฉันทำเครื่องหมายไว้มันเป็นครั้งแรกอย่างน้อยก็เร็วเท่าที่สอง ค่อนข้างแปลกใจบางครั้งมันก็เร็วขึ้น (เช่นใน Sun Sparc) เมื่อฮาร์ดแวร์ไม่รองรับการคูณอย่างรวดเร็ว (และส่วนใหญ่ไม่ได้กลับมาแล้ว) คอมไพเลอร์จะแปลงการคูณเป็นชุดค่าผสมที่เหมาะสมของกะและเพิ่ม / ย่อย และเนื่องจากรู้เป้าหมายสุดท้ายบางครั้งจึงทำได้ในคำสั่งน้อยกว่าเมื่อคุณเขียนกะและการเพิ่ม / การย่อยอย่างชัดเจน

โปรดทราบว่านี่เป็นสิ่งที่เมื่อ 15 ปีที่แล้ว หวังว่าคอมไพเลอร์จะได้ดีขึ้นตั้งแต่นั้นมาดังนั้นคุณสามารถนับได้ว่าคอมไพเลอร์ทำสิ่งที่ถูกต้องอาจดีกว่าที่คุณจะทำได้ (นอกจากนี้เหตุผลที่รหัสดู C'ish เป็นเพราะมันมากกว่า 15 ปีที่แล้วฉันเห็นได้ชัดว่าฉันใช้std::stringและตัววนซ้ำวันนี้)


5
คุณอาจสนใจโพสต์บล็อกต่อไปนี้ซึ่งผู้เขียนตั้งข้อสังเกตว่าคอมไพเลอร์การเพิ่มประสิทธิภาพที่ทันสมัยดูเหมือนจะย้อนกลับวิศวกรรูปแบบทั่วไปที่โปรแกรมเมอร์อาจใช้การคิดให้มีประสิทธิภาพมากขึ้นในรูปแบบทางคณิตศาสตร์ของพวกเขาเพื่อสร้างลำดับการสอนที่มีประสิทธิภาพมากที่สุด . shape-of-code.coding-guidelines.com/2009/06/30/…
Pascal Cuoq

@PascalCuoq ไม่มีอะไรใหม่เกี่ยวกับเรื่องนี้ ฉันค้นพบสิ่งเดียวกันสำหรับ Sun CC เกือบ 20 ปีที่แล้ว
James Kanze

67

นอกจากคำตอบที่ดีอื่น ๆ ทั้งหมดที่นี่ให้ฉันชี้ให้เห็นเหตุผลอื่นที่จะไม่ใช้กะเมื่อคุณหมายถึงการหารหรือคูณ ฉันไม่เคยเห็นใครสักคนแนะนำข้อผิดพลาดโดยลืมลำดับความสำคัญของการคูณและการเพิ่ม ฉันได้เห็นข้อบกพร่องแนะนำเมื่อโปรแกรมเมอร์บำรุงรักษาลืมไปว่า "คูณ" ผ่านการเปลี่ยนแปลงเป็นเหตุผลคูณ แต่ไม่ไวยากรณ์ของความสำคัญเช่นเดียวกับการคูณ x * 2 + zและx << 1 + zแตกต่างกันมาก!

หากคุณกำลังทำงานเกี่ยวกับตัวเลข+ - * / %แล้วใช้ดำเนินการทางคณิตศาสตร์เช่น หากคุณกำลังทำงานบนอาร์เรย์ของบิตใช้บิต twiddling & ^ | >>ผู้ประกอบการเช่น อย่าผสมพวกเขา การแสดงออกที่มีทั้งสองนิด ๆ หน่อย ๆ และเลขคณิตเป็นข้อผิดพลาดรอที่จะเกิดขึ้น


5
หลีกเลี่ยงได้ด้วยวงเล็บอย่างง่าย?
Joel B

21
@ Joel: แน่นอน ถ้าคุณจำได้ว่าคุณต้องการพวกเขา ประเด็นของฉันคือมันง่ายที่จะลืมว่าคุณทำ คนที่มีนิสัยชอบอ่านหนังสือ "x << 1" ราวกับว่ามันเป็น "x * 2" เข้าสู่นิสัยที่คิดว่า << นั้นมีความสำคัญเหมือนกับการคูณซึ่งไม่ใช่
Eric Lippert

1
ฉันพบการแสดงออก "(สวัสดี << 8) + lo" เปิดเผยโดยเจตนามากกว่า "hi * 256 + lo" อาจเป็นเรื่องของรสนิยม แต่บางครั้งก็ชัดเจนมากขึ้นในการเขียน bit-twiddling ในกรณีส่วนใหญ่แม้ว่าฉันเห็นด้วยกับจุดของคุณโดยสิ้นเชิง
Ivan Danilov

32
@Ivan: และ "(สวัสดี << 8) | lo" ชัดเจนยิ่งขึ้น การตั้งค่าบิตต่ำของบิตอาเป็นไม่ได้นอกจากของจำนวนเต็ม มันคือการตั้งค่าบิตดังนั้นเขียนรหัสที่ตั้งบิต
Eric Lippert

1
ว้าว. ไม่เคยคิดอย่างนี้มาก่อน ขอบคุณ
Ivan Danilov

50

ขึ้นอยู่กับโปรเซสเซอร์และคอมไพเลอร์ คอมไพเลอร์บางตัวปรับโค้ดให้เหมาะสมแล้ว ดังนั้นคุณต้องตรวจสอบทุกครั้งที่รหัสของคุณจำเป็นต้องได้รับการปรับปรุงด้วยวิธีนี้

ถ้าคุณไม่ต้องการปรับให้เหมาะสมฉันจะไม่แย่งรหัสต้นฉบับของฉันเพียงเพื่อบันทึกคำสั่งชุดประกอบหรือวงจรประมวลผล


3
เพียงเพื่อเพิ่มการประมาณคร่าวๆ: ในโปรเซสเซอร์ 16 บิตทั่วไป (80C166) การเพิ่มสอง ints มาที่ 1-2 รอบคูณด้วย 10 รอบและหารที่ 20 รอบ บวกการย้ายการดำเนินการบางอย่างถ้าคุณปรับ i * 10 ให้เป็นหลาย ops (แต่ละ mov อีก +1 รอบ) คอมไพเลอร์ที่พบบ่อยที่สุด (Keil / Tasking) ไม่ปรับให้เหมาะสมเว้นแต่การคูณ / ดิวิชั่นด้วยพลังของ 2
Jens

55
และโดยทั่วไปคอมไพเลอร์ปรับโค้ดให้เหมาะสมดีกว่าที่คุณทำ
user703016

ฉันยอมรับว่าเมื่อคูณ "ปริมาณ" ตัวดำเนินการคูณจะดีกว่าโดยทั่วไป แต่เมื่อแบ่งค่าที่ลงชื่อด้วยพลัง 2 แล้วตัว>>ดำเนินการจะเร็วกว่า/และถ้าค่าที่เซ็นชื่อสามารถเป็นลบได้ หากเราต้องการคุณค่าที่x>>4จะสร้างขึ้นมันก็ชัดเจนกว่าx < 0 ? -((-1-x)/16)-1 : x/16;และฉันไม่สามารถจินตนาการได้ว่าคอมไพเลอร์สามารถปรับการแสดงออกของสิ่งนั้นให้เป็นสิ่งที่ดีได้อย่างไร
supercat

38

จริง ๆ แล้วมันเร็วกว่าที่จะใช้พูด (i << 3) + (i << 1) เพื่อคูณด้วย 10 แทนที่จะใช้ i * 10 โดยตรงหรือไม่

อาจเป็นหรือไม่ได้อยู่ในเครื่องของคุณ - ถ้าคุณสนใจให้วัดการใช้งานจริงของคุณ

กรณีศึกษา - จาก 486 ถึง Core i7

การเปรียบเทียบเป็นเรื่องยากมากที่จะทำอย่างมีความหมาย แต่เราสามารถดูข้อเท็จจริงบางอย่างได้ จากhttp://www.penguin.cz/~literakl/intel/s.html#SALและ http://www.penguin.cz/~literakl/intel/i.html#IMULเราทราบถึงวงจรนาฬิกา x86 จำเป็นสำหรับการเปลี่ยนแปลงทางคณิตศาสตร์และการคูณ สมมติว่าเรายึดติดกับ "486" (รายการใหม่ล่าสุดที่จดทะเบียน), 32 บิตการลงทะเบียนและทันที IMUL ใช้เวลา 13-42 รอบและ IDIV 44 SAL แต่ละอันใช้เวลา 2 และเพิ่ม 1 ดังนั้นแม้จะมีไม่กี่คน เหมือนผู้ชนะ

วันนี้ด้วย Core i7:

(จากhttp://software.intel.com/en-us/forums/showthread.php?t=61481 )

แฝงเป็น1 รอบสำหรับนอกจากจำนวนเต็มและ 3 รอบสำหรับการคูณจำนวนเต็ม คุณสามารถค้นหาศักยภาพและ thoughput ในภาคผนวก C ของ "Intel® 64 และ IA-32 สถาปัตยกรรมการเพิ่มประสิทธิภาพ Reference Manual" ซึ่งตั้งอยู่บนhttp://www.intel.com/products/processor/manuals/

(จากบาง Intel แจ้งแจ้งความ)

การใช้ SSE นั้น Core i7 สามารถออกคำสั่งการเพิ่มและทวีคูณพร้อมกันส่งผลให้อัตราการดำเนินการจุดลอยตัว (FLOP) สูงสุด 8 ครั้งต่อรอบนาฬิกา

ที่ให้คุณทราบว่าสิ่งต่าง ๆ มาไกลแค่ไหน เรื่องเล็กน้อยเรื่องการปรับให้เหมาะสม - เช่นการเปลี่ยนบิตเมื่อเทียบกับ*- ซึ่งได้รับการดำเนินการอย่างจริงจังแม้กระทั่งใน 90s นั้นล้าสมัยไปแล้วในตอนนี้ การเลื่อนบิตยังเร็วกว่า แต่สำหรับการเปลี่ยนแปลงที่ไม่ใช่พลังของสอง / เวลาตามเวลาที่คุณทำกะทั้งหมดและเพิ่มผลลัพธ์มันช้าลงอีกครั้ง จากนั้นคำแนะนำเพิ่มเติมหมายถึงข้อผิดพลาดแคชเพิ่มเติมปัญหาที่อาจเกิดขึ้นในการวางท่อการลงทะเบียนชั่วคราวมากขึ้นอาจหมายถึงการประหยัดและการคืนค่าเนื้อหาการลงทะเบียนจากสแต็กมากขึ้น ... มันซับซ้อนเกินไปอย่างรวดเร็วในการประเมินผลกระทบทั้งหมด ลบส่วนใหญ่

การทำงานในซอร์สโค้ดเทียบกับการนำไปใช้

โดยทั่วไปคำถามของคุณจะถูกแท็ก C และ C ++ ในฐานะภาษารุ่นที่ 3 พวกเขาได้รับการออกแบบมาโดยเฉพาะเพื่อซ่อนรายละเอียดของชุดคำสั่ง CPU พื้นฐาน เพื่อตอบสนองความมาตรฐานภาษาของพวกเขาพวกเขาจะต้องสนับสนุนการคูณและการดำเนินงานขยับ (และอื่น ๆ อีกมากมาย) แม้ว่าฮาร์ดแวร์พื้นฐานไม่ได้ ในกรณีเช่นนี้พวกเขาจะต้องสังเคราะห์ผลลัพธ์ที่ต้องการโดยใช้คำแนะนำอื่น ๆ อีกมากมาย ในทำนองเดียวกันพวกเขาจะต้องให้การสนับสนุนซอฟต์แวร์สำหรับการดำเนินการจุดลอยหาก CPU ขาดและไม่มี FPU CPU สมัยใหม่รองรับ*และ<<ดังนั้นสิ่งนี้อาจดูเหมือนเป็นเรื่องเหลวไหลทั้งทางทฤษฎีและประวัติศาสตร์ แต่สิ่งสำคัญคือเสรีภาพในการเลือกใช้งานนั้นมีสองวิธี: แม้ว่า CPU จะมีคำสั่งที่ใช้การดำเนินการที่ร้องขอในซอร์สโค้ดในกรณีทั่วไปคอมไพเลอร์ไม่มีค่าใช้จ่าย เลือกอย่างอื่นที่ชอบเพราะดีกว่าสำหรับกรณีเฉพาะที่คอมไพเลอร์ต้องเผชิญ

ตัวอย่าง (ด้วยภาษาประกอบสมมุติ)

source           literal approach         optimised approach
#define N 0
int x;           .word x                xor registerA, registerA
x *= N;          move x -> registerA
                 move x -> registerB
                 A = B * immediate(0)
                 store registerA -> x
  ...............do something more with x...............

คำแนะนำอย่างเอกสิทธิ์เฉพาะบุคคลหรือ ( xor) ไม่มีความสัมพันธ์กับซอร์สโค้ด แต่ xor-ing อะไรที่ตัวเองล้างบิตทั้งหมดดังนั้นจึงสามารถใช้เพื่อตั้งค่าบางอย่างเป็น 0 ซอร์สโค้ดที่บอกถึงที่อยู่หน่วยความจำอาจไม่เกี่ยวข้องกับการใช้งาน

แฮ็กชนิดนี้ใช้งานได้นานเท่าที่คอมพิวเตอร์ใช้งาน ในวันแรก ๆ ของ 3GLs เพื่อความปลอดภัยของนักพัฒนาในการทำความเข้าใจเอาท์พุทคอมไพเลอร์ต้องตอบสนองการพัฒนาภาษาแอสเซมบลีที่ใช้งานง่ายด้วยมือที่มีอยู่เดิม ชุมชนที่รหัสที่ผลิตไม่ได้ช้ากว่า verbose มากขึ้นหรือแย่ลง คอมไพเลอร์ปรับใช้การเพิ่มประสิทธิภาพที่ยอดเยี่ยมอย่างรวดเร็ว - พวกเขากลายเป็นศูนย์กลางการจัดเก็บที่ดีกว่าโปรแกรมเมอร์ภาษาแอสเซมบลีแต่ละตัวที่อาจเป็นไปได้แม้ว่าจะมีโอกาสที่พวกเขาจะพลาดการปรับแต่งเฉพาะที่เกิดขึ้น ดึงออกมาแล้วคว้านหาอะไรที่ดีกว่าในขณะที่คอมไพเลอร์ทำตามที่พวกเขาได้รับการบอกกล่าวจนกว่าจะมีคนดึงข้อมูลที่ได้รับประสบการณ์กลับคืนมา

ดังนั้นแม้ว่าการขยับและการเพิ่มยังเร็วกว่าสำหรับฮาร์ดแวร์บางตัวนักเขียนคอมไพเลอร์ก็มีแนวโน้มที่จะทำงานได้อย่างถูกต้องเมื่อมันปลอดภัยและเป็นประโยชน์

การบำรุงรักษา

หากการเปลี่ยนแปลงฮาร์ดแวร์ของคุณคุณสามารถคอมไพล์ใหม่และมันจะดู CPU เป้าหมายและเป็นทางเลือกที่ดีที่สุดในขณะที่คุณไม่ต้องการที่จะกลับมา "optimisations" ของคุณหรือรายการที่สภาพแวดล้อมการรวบรวมควรใช้การคูณและควรเปลี่ยน ลองนึกถึง "การเพิ่มประสิทธิภาพ" ที่ไม่ได้ใช้กำลังของสองบิตที่ได้รับการเขียนเมื่อ 10+ ปีที่แล้วซึ่งตอนนี้กำลังทำให้โค้ดช้าลงเพราะมันทำงานบนโปรเซสเซอร์ที่ทันสมัย ​​... !

โชคดีที่คอมไพเลอร์ที่ดีเช่น GCC สามารถแทนที่ชุดของบิตกะและเลขคณิตด้วยการคูณโดยตรงเมื่อเปิดใช้งานการเพิ่มประสิทธิภาพ (เช่น...main(...) { return (argc << 4) + (argc << 2) + argc; }-> imull $21, 8(%ebp), %eax) ดังนั้นการคอมไพล์ใหม่อาจช่วยได้โดยไม่ต้องแก้ไขโค้ด แต่ก็ไม่รับประกัน

รหัสบิตการเปลี่ยนรหัสที่ใช้การคูณหรือการแบ่งเป็นสิ่งที่แสดงให้เห็นถึงสิ่งที่คุณพยายามทำสำเร็จดังนั้นนักพัฒนาคนอื่น ๆ จะสับสนและนักเขียนโปรแกรมที่สับสนมักจะแนะนำบั๊กหรือกำจัดสิ่งที่จำเป็นในการฟื้นฟูสติ หากคุณเพียง แต่ทำสิ่งที่ไม่ชัดเจนเมื่อพวกมันมีประโยชน์เป็นรูปธรรมจริงๆและจากนั้นให้บันทึกไว้อย่างดี (แต่ไม่ต้องบันทึกสิ่งอื่นที่ใช้งานง่าย) ทุกคนจะมีความสุข

โซลูชันทั่วไปกับโซลูชันบางส่วน

หากคุณมีความรู้พิเศษเช่นคุณintจริงๆจะได้รับการจัดเก็บค่าx, yและzแล้วคุณอาจจะไม่สามารถที่จะทำงานออกคำแนะนำบางอย่างที่ทำงานสำหรับค่าเหล่านั้นและคุณได้รับผลของคุณมากขึ้นอย่างรวดเร็วกว่าเมื่อคอมไพเลอร์ไม่ได้มี ข้อมูลเชิงลึกนั้นและจำเป็นต้องมีการใช้งานที่ทำงานได้กับintค่าทั้งหมด ตัวอย่างเช่นพิจารณาคำถามของคุณ:

การคูณและการหารสามารถทำได้โดยใช้ตัวดำเนินการบิต ...

คุณแสดงการคูณ แต่วิธีการหาร?

int x;
x >> 1;   // divide by 2?

ตามมาตรฐาน C ++ 5.8:

-3- ค่าของ E1 >> E2 คือตำแหน่ง E1 ที่เลื่อนไปทางขวา E1 หาก E1 มีประเภทที่ไม่ได้ลงนามหรือถ้า E1 มีประเภทที่ลงนามและค่าที่ไม่ใช่ค่าลบค่าของผลลัพธ์จะเป็นส่วนที่สำคัญของความฉลาดทาง E1 หารด้วยปริมาณ 2 ยกกำลัง E2 หาก E1 มีประเภทที่ลงนามและค่าลบค่าผลลัพธ์จะถูกกำหนดโดยการนำไปปฏิบัติ

ดังนั้นการเปลี่ยนบิตของคุณจะมีผลการดำเนินการตามที่กำหนดเมื่อxเป็นลบ: มันอาจไม่ทำงานในลักษณะเดียวกันกับเครื่องที่แตกต่าง แต่/คาดการณ์ได้ดีกว่ามาก (มันอาจจะไม่สอดคล้องกันอย่างสมบูรณ์เช่นกันเนื่องจากเครื่องจักรต่าง ๆ อาจมีตัวเลขแทนจำนวนต่างกันและช่วงที่แตกต่างกันแม้ว่าจะมีบิตจำนวนเท่ากันในการเป็นตัวแทน)

คุณอาจพูดว่า "ฉันไม่สนใจ ... นั่นintคือการเก็บอายุของพนักงานมันจะไม่เป็นลบ" หากคุณมีข้อมูลเชิงลึกพิเศษเช่นนั้นใช่ - >>การเพิ่มประสิทธิภาพที่ปลอดภัยของคุณอาจถูกส่งผ่านโดยคอมไพเลอร์เว้นแต่คุณจะทำอย่างชัดเจนในรหัสของคุณ แต่มันมีความเสี่ยงและไม่ค่อยมีประโยชน์เท่าที่คุณจะไม่มีความเข้าใจเช่นนี้และโปรแกรมเมอร์คนอื่น ๆ ที่ทำงานด้วยรหัสเดียวกันจะไม่ทราบว่าคุณวางเดิมพันบนบ้านด้วยความคาดหวังที่ผิดปกติของข้อมูลที่คุณ ' จะจัดการ ... สิ่งที่ดูเหมือนว่าการเปลี่ยนแปลงที่ปลอดภัยอย่างสมบูรณ์สำหรับพวกเขาอาจย้อนกลับมาได้เนื่องจาก "การเพิ่มประสิทธิภาพ" ของคุณ

มีอินพุตอะไรบ้างที่ไม่สามารถคูณหรือหารด้วยวิธีนี้ได้?

ใช่ ... ดังที่ได้กล่าวไปแล้วจำนวนลบมีพฤติกรรมการใช้งานที่กำหนดไว้เมื่อ "หาร" โดยการเลื่อนบิต


2
คำตอบที่ดีมาก Core i7 เทียบกับ 486 เปรียบเทียบกำลังตรัสรู้!
Drew Hall

ในสถาปัตยกรรมสามัญทั้งหมดintVal>>1จะมีความหมายเดียวกันซึ่งแตกต่างจากสถาปัตยกรรมintVal/2ที่มีประโยชน์ในบางครั้ง หากหนึ่งในความต้องการในการคำนวณในแฟชั่นแบบพกพาค่าที่สถาปัตยกรรมธรรมดาจะให้ผลผลิตสำหรับการแสดงออกจะต้องค่อนข้างซับซ้อนมากขึ้นและยากที่จะอ่านและจะมีแนวโน้มที่จะสร้างรหัสที่ด้อยกว่าอย่างมีนัยสำคัญที่ผลิตเพื่อintVal >> 1 intVal >> 1
supercat

35

ลองใช้เครื่องของฉันในการรวบรวมสิ่งนี้:

int a = ...;
int b = a * 10;

เมื่อแยกส่วนออกจะสร้างเอาต์พุต:

MOV EAX,DWORD PTR SS:[ESP+1C] ; Move a into EAX
LEA EAX,DWORD PTR DS:[EAX+EAX*4] ; Multiply by 5 without shift !
SHL EAX, 1 ; Multiply by 2 using shift

รุ่นนี้เร็วกว่าโค้ดที่เพิ่มประสิทธิภาพด้วยมือของคุณด้วยการเลื่อนและการเพิ่ม

คุณไม่มีทางรู้ได้เลยว่าคอมไพเลอร์กำลังจะเกิดอะไรขึ้นดังนั้นจึงเป็นการดีกว่าที่จะเขียนการคูณปกติและให้เขาปรับวิธีที่เขาต้องการยกเว้นในกรณีที่คุณรู้ว่าคอมไพเลอร์ไม่สามารถปรับให้เหมาะสม


1
คุณจะได้รับการโหวตขนาดใหญ่สำหรับเรื่องนี้ถ้าคุณข้ามส่วนที่เกี่ยวกับเวกเตอร์ หากคอมไพเลอร์สามารถแก้ไขทวีคูณก็จะเห็นว่าเวกเตอร์ไม่เปลี่ยนแปลง
โบเพอร์สัน

คอมไพเลอร์จะรู้ได้อย่างไรว่าขนาดเวกเตอร์จะไม่เปลี่ยนแปลงหากไม่มีการตั้งสมมติฐานที่อันตรายจริงๆ หรือมีคุณไม่เคยได้ยินเห็นพ้องด้วย ...
ชาร์ลส์กูดวิน

1
ตกลงดังนั้นคุณวนลูปเวกเตอร์ระดับโลกที่ไม่มีล็อคหรือไม่ และฉันวนลูปเวกเตอร์ในพื้นที่ซึ่งยังไม่ได้รับที่อยู่และเรียกใช้ฟังก์ชันสมาชิก const เท่านั้น อย่างน้อยคอมไพเลอร์ของฉันตระหนักว่าขนาดเวกเตอร์จะไม่เปลี่ยนแปลง (และอีกไม่นานมีคนอาจตั้งค่าสถานะเราเพื่อแชท :-)
โบเพอร์สัน

1
@BoPersson ในที่สุดหลังจากเวลาทั้งหมดนี้ฉันลบคำสั่งของฉันเกี่ยวกับการรวบรวมไม่สามารถเพิ่มประสิทธิภาพออกไป vector<T>::size()สุดท้ายหลังจากที่ทุกเวลานี้ฉันออกคำสั่งของฉันเกี่ยวกับคอมไพเลอร์จะไม่สามารถที่จะเพิ่มประสิทธิภาพออกไป ผู้เรียบเรียงของฉันค่อนข้างโบราณ! :)
user703016

21

โดยทั่วไปการเลื่อนความเร็วจะเร็วกว่าการคูณในระดับการเรียนการสอน แต่คุณอาจเสียเวลาในการปรับให้เหมาะสมก่อนเวลาอันควร คอมไพเลอร์อาจทำการปรับให้เหมาะสมเหล่านี้ได้ในเวลารวบรวม การทำด้วยตัวเองจะส่งผลกระทบต่อความสามารถในการอ่านและอาจไม่มีผลต่อประสิทธิภาพการทำงาน มันอาจคุ้มค่าที่จะทำสิ่งนี้ถ้าคุณทำประวัติและพบว่าสิ่งนี้เป็นคอขวด

ในความเป็นจริงเคล็ดลับการแบ่งที่เรียกว่า 'การแบ่งเวทมนตร์' สามารถให้ผลตอบแทนมหาศาล คุณควรทำโปรไฟล์อีกครั้งก่อนเพื่อดูว่าจำเป็นหรือไม่ แต่ถ้าคุณใช้มันจะมีโปรแกรมที่มีประโยชน์อยู่รอบ ๆ เพื่อช่วยให้คุณทราบว่าจำเป็นต้องใช้คำแนะนำสำหรับซีแมนทิกส์เดียวกันหรือไม่ นี่คือตัวอย่าง: http://www.masm32.com/board/index.php?topic=12421.0

ตัวอย่างที่ฉันยกขึ้นจากเธรดของ OP บน MASM32:

include ConstDiv.inc
...
mov eax,9999999
; divide eax by 100000
cdiv 100000
; edx = quotient

จะสร้าง:

mov eax,9999999
mov edx,0A7C5AC47h
add eax,1
.if !CARRY?
    mul edx
.endif
shr edx,16

7
@ ดึงด้วยเหตุผลบางอย่างความคิดเห็นของคุณทำให้ฉันหัวเราะและทำกาแฟหก ขอบคุณ
asawyer

30
ไม่มีกระทู้ในฟอรัมที่เกี่ยวกับความชอบคณิตศาสตร์ ทุกคนที่ชอบคณิตศาสตร์รู้ว่ามันยากแค่ไหนที่จะสร้างกระทู้ฟอรัมที่ "สุ่ม" จริง
Joel B

1
มันอาจเป็นเพียงคุ้มค่าที่จะทำสิ่งเช่นนี้หากคุณมีประวัติและพบนี้จะเป็นคอขวด และดำเนินการทางเลือกและรายละเอียดอีกครั้งและได้รับประโยชน์จากประสิทธิภาพการทำงานอย่างน้อย 10 ครั้ง
Lie Ryan

12

คำแนะนำการคูณแบบ Shift และจำนวนเต็มมีประสิทธิภาพคล้ายกันกับ CPU ที่ทันสมัยส่วนใหญ่คำแนะนำการคูณแบบจำนวนเต็มค่อนข้างย้อนกลับไปค่อนข้างช้าในช่วงปี 1980 แต่โดยทั่วไปสิ่งนี้ไม่เป็นความจริงอีกต่อไป คำแนะนำการคูณจำนวนเต็มอาจมีเวลาแฝงสูงกว่าดังนั้นอาจยังมีบางกรณีที่มีการเลื่อนตำแหน่งที่ดีกว่า เช่นเดียวกันกับกรณีที่คุณสามารถรักษาหน่วยการทำงานให้ยุ่งได้มากขึ้น (แม้ว่าจะสามารถลดทั้งสองวิธีได้)

การแบ่งจำนวนเต็มยังคงค่อนข้างช้าดังนั้นการใช้การเปลี่ยนแทนการหารด้วยกำลัง 2 ยังคงเป็นชัยชนะและคอมไพเลอร์ส่วนใหญ่จะใช้สิ่งนี้เป็นการปรับให้เหมาะสม อย่างไรก็ตามโปรดทราบว่าสำหรับการเพิ่มประสิทธิภาพนี้ให้ถูกต้องเงินปันผลจะต้องไม่ได้ลงนามหรือจะต้องเป็นบวก สำหรับเงินปันผลที่เป็นลบการเปลี่ยนแปลงและการแบ่งไม่เท่ากัน!

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 5; i >= -5; --i)
    {
        printf("%d / 2 = %d, %d >> 1 = %d\n", i, i / 2, i, i >> 1);
    }
    return 0;
}

เอาท์พุท:

5 / 2 = 2, 5 >> 1 = 2
4 / 2 = 2, 4 >> 1 = 2
3 / 2 = 1, 3 >> 1 = 1
2 / 2 = 1, 2 >> 1 = 1
1 / 2 = 0, 1 >> 1 = 0
0 / 2 = 0, 0 >> 1 = 0
-1 / 2 = 0, -1 >> 1 = -1
-2 / 2 = -1, -2 >> 1 = -1
-3 / 2 = -1, -3 >> 1 = -2
-4 / 2 = -2, -4 >> 1 = -2
-5 / 2 = -2, -5 >> 1 = -3

ดังนั้นหากคุณต้องการช่วยคอมไพเลอร์ตรวจสอบให้แน่ใจว่าตัวแปรหรือนิพจน์ในการจ่ายเงินปันผลนั้นไม่ได้ลงนามอย่างชัดเจน


4
การคูณจำนวนเต็มจะเป็นไมโครโค้ดเช่น PPU ของ PlayStation 3 และหยุดการผลิตทั้งหมด ก็แนะนำให้หลีกเลี่ยงการจำนวนเต็มคูณบนแพลตฟอร์มบางส่วนยังคง :)
Maister

2
หน่วยงานที่ไม่ได้ลงนามจำนวนมากคือ - สมมติว่าคอมไพเลอร์รู้วิธีการใช้งานโดยใช้ตัวคูณที่ไม่ได้ลงนาม หนึ่งหรือสองทวีคูณ @ รอบนาฬิกาไม่กี่รอบแต่ละสามารถทำงานเช่นเดียวกับส่วน @ 40 รอบแต่ละขึ้นไป
Olof Forshell

1
@Olof: จริง แต่ถูกต้องสำหรับการหารด้วยค่าคงที่เวลารวบรวมแน่นอน
Paul R

4

มันขึ้นอยู่กับอุปกรณ์เป้าหมายภาษาวัตถุประสงค์ ฯลฯ

Pixel crunching ในไดรเวอร์การ์ดแสดงผลหรือไม่ มีโอกาสมากใช่!

. NET application สำหรับแผนกของคุณ? ไม่มีเหตุผลเลยที่จะมองมัน

สำหรับเกมที่มีประสิทธิภาพสูงสำหรับอุปกรณ์พกพามันอาจคุ้มค่าที่จะมอง แต่หลังจากทำการปรับให้เหมาะสมที่ง่ายขึ้นแล้ว


2

อย่าทำอย่างนั้นถ้าคุณไม่ต้องการจริงๆและเจตนาของโค้ดของคุณต้องการการขยับมากกว่าการคูณ / การหาร

ในวันปกติคุณสามารถประหยัดรอบเครื่องได้สองสามรอบ (หรือหลวมเนื่องจากคอมไพเลอร์รู้ดีว่าต้องเพิ่มประสิทธิภาพอะไร) แต่ค่าใช้จ่ายไม่คุ้มค่า - คุณใช้เวลากับรายละเอียดเล็กน้อยมากกว่างานจริงการบำรุงรักษารหัสจะยากขึ้นและ เพื่อนร่วมงานของคุณจะสาปแช่งคุณ

คุณอาจต้องทำการคำนวณความเร็วสูงโดยที่แต่ละรอบการบันทึกหมายถึงจำนวนนาทีของการใช้งานจริง แต่คุณควรเพิ่มประสิทธิภาพทีละสถานที่และทำการทดสอบประสิทธิภาพในแต่ละครั้งเพื่อดูว่าคุณทำให้มันเร็วขึ้นหรือตรรกะของคอมไพเลอร์เสียจริงหรือไม่


1

เท่าที่ฉันรู้ในการคูณเครื่องอาจต้องใช้รอบเครื่องมากถึง 16 ถึง 32 รอบ ดังนั้นใช่ขึ้นอยู่กับชนิดของเครื่องตัวดำเนินการบิตเชนรวดเร็วกว่าการคูณ / การหาร

อย่างไรก็ตามเครื่องบางเครื่องมีตัวประมวลผลทางคณิตศาสตร์ซึ่งมีคำแนะนำพิเศษสำหรับการคูณ / การหาร


7
คนที่เขียนคอมไพเลอร์สำหรับเครื่องเหล่านั้นก็น่าจะอ่านแฮ็กเกอร์ดีไลท์และปรับให้เหมาะสม
โบเพอร์สัน

1

ฉันเห็นด้วยกับคำตอบที่ทำเครื่องหมายไว้โดย Drew Hall คำตอบอาจใช้หมายเหตุเพิ่มเติมบางอย่าง

สำหรับนักพัฒนาซอฟต์แวร์ส่วนใหญ่โปรเซสเซอร์และคอมไพเลอร์ไม่เกี่ยวข้องกับคำถามอีกต่อไป พวกเราส่วนใหญ่อยู่ไกลเกินกว่า 8088 และ MS-DOS อาจเกี่ยวข้องกับผู้ที่ยังคงพัฒนาตัวประมวลผลแบบฝังอยู่เท่านั้น ...

ควรใช้คณิตศาสตร์ที่ บริษัท ซอฟต์แวร์ของฉัน (เพิ่ม / sub / mul / div) สำหรับคณิตศาสตร์ทั้งหมด ในขณะที่ควรใช้ Shift เมื่อแปลงระหว่างชนิดข้อมูลเช่น ใช้ร่วมกับไบต์เป็น n >> 8 และไม่ใช่ n / 256


ฉันเห็นด้วยกับคุณเช่นกัน ฉันทำตามแนวทางเดียวกันโดยไม่รู้ตัวแม้ว่าฉันจะไม่ได้มีข้อกำหนดอย่างเป็นทางการที่จะทำเช่นนั้น
Drew Hall

0

ในกรณีของจำนวนเต็มที่ลงนามและการเปลี่ยนด้านขวาเทียบกับการหารมันสามารถสร้างความแตกต่างได้ สำหรับตัวเลขติดลบการเลื่อนจะปัดเศษไปทางอนันต์ลบในขณะที่การปัดเศษเป็นศูนย์ แน่นอนว่าคอมไพเลอร์จะเปลี่ยนการแบ่งเป็นบางสิ่งที่ถูกกว่า แต่โดยปกติแล้วมันจะเปลี่ยนเป็นสิ่งที่มีพฤติกรรมการปัดเศษแบบเดียวกับการหารเพราะมันไม่สามารถพิสูจน์ได้ว่าตัวแปรจะไม่เป็นลบหรือไม่เพียงแค่นั้น การดูแล ดังนั้นหากคุณสามารถพิสูจน์ได้ว่าตัวเลขจะไม่เป็นลบหรือถ้าคุณไม่สนใจว่ามันจะปัดเศษแบบไหนคุณสามารถทำการปรับให้เหมาะสมด้วยวิธีที่มีแนวโน้มที่จะสร้างความแตกต่าง


หรือโยนหมายเลขไปที่unsigned
Lie Ryan

4
คุณแน่ใจหรือว่าพฤติกรรมการขยับได้มาตรฐาน? ฉันอยู่ภายใต้การแสดงผลว่าการเปลี่ยน ints เชิงลบเป็นการกำหนดการนำไปใช้
Kerrek SB

1
ในขณะที่คุณควรพูดถึงรหัสที่อาศัยพฤติกรรมเฉพาะใด ๆ สำหรับตัวเลขติดลบที่เปลี่ยนไปทางขวาควรจัดทำเอกสารความต้องการนั้นข้อดีของการเปลี่ยนที่อยู่ด้านขวานั้นมีขนาดใหญ่มากในกรณีที่มันให้คุณค่าที่ถูกต้องตามธรรมชาติ การคำนวณเวลาด้วยค่าที่ไม่พึงประสงค์ซึ่งรหัสผู้ใช้จะต้องเสียเวลาเพิ่มเติมในการปรับเพื่อให้ได้สิ่งที่การเปลี่ยนแปลงจะได้รับในครั้งแรก อันที่จริงถ้าผมมี druthers ของฉันคอมไพเลอร์จะมีตัวเลือกในการร้องทุกข์ที่ความพยายามในการที่จะดำเนินการส่วนการลงนามตั้งแต่ ...
SuperCat

1
... รหัสที่รู้ว่าตัวถูกดำเนินการเป็นบวกสามารถปรับปรุงการเพิ่มประสิทธิภาพถ้ามันถูกส่งไปยังไม่ได้ลงชื่อก่อนการหาร (อาจส่งกลับไปยังการเซ็นชื่อหลังจากนั้น) และรหัสที่รู้ว่าตัวถูกดำเนินการอาจเป็นค่าลบควรจัดการกับกรณีดังกล่าวอย่างชัดเจน คนหนึ่งอาจคิดว่าพวกเขาเป็นบวก)
supercat

0

การทดสอบของ Python จะทำการคูณที่เหมือนกัน 100 ล้านครั้งกับตัวเลขสุ่มที่เหมือนกัน

>>> from timeit import timeit
>>> setup_str = 'import scipy; from scipy import random; scipy.random.seed(0)'
>>> N = 10*1000*1000
>>> timeit('x=random.randint(65536);', setup=setup_str, number=N)
1.894096851348877 # Time from generating the random #s and no opperati

>>> timeit('x=random.randint(65536); x*2', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); x << 1', setup=setup_str, number=N)
2.2616429328918457

>>> timeit('x=random.randint(65536); x*10', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); (x << 3) + (x<<1)', setup=setup_str, number=N)
2.9485139846801758

>>> timeit('x=random.randint(65536); x // 2', setup=setup_str, number=N)
2.490908145904541
>>> timeit('x=random.randint(65536); x / 2', setup=setup_str, number=N)
2.4757170677185059
>>> timeit('x=random.randint(65536); x >> 1', setup=setup_str, number=N)
2.2316000461578369

ดังนั้นในการทำการเปลี่ยนแปลงมากกว่าการคูณ / การหารด้วยพลังสองในงูใหญ่มีการปรับปรุงเล็กน้อย (~ 10% สำหรับการหาร; ~ 1% สำหรับการคูณ) หากไม่ใช่พลังของทั้งสองอาจมีการชะลอตัวมาก

#s เหล่านี้อีกครั้งจะเปลี่ยนแปลงขึ้นอยู่กับโปรเซสเซอร์ของคุณคอมไพเลอร์ของคุณ (หรือล่าม - ทำในหลามเพื่อความเรียบง่าย)

เช่นเดียวกับคนอื่น ๆ อย่าเพิ่มประสิทธิภาพก่อนเวลาอันควร เขียนโค้ดที่อ่านได้มากโปรไฟล์ถ้ามันไม่เร็วพอจากนั้นลองเพิ่มประสิทธิภาพส่วนที่ช้า โปรดจำไว้ว่าคอมไพเลอร์ของคุณดีกว่าการเพิ่มประสิทธิภาพมากกว่าที่คุณเป็น


0

มีการปรับให้เหมาะสมที่คอมไพเลอร์ไม่สามารถทำได้เพราะมันทำงานเฉพาะกับชุดอินพุตที่ลดลงเท่านั้น

ด้านล่างมีโค้ดตัวอย่าง c ++ ที่สามารถทำการหารได้เร็วขึ้นโดยทำ 64 บิต "การคูณด้วยส่วนกลับ" ทั้งตัวเศษและส่วนจะต้องต่ำกว่าเกณฑ์ที่กำหนด โปรดทราบว่าจะต้องรวบรวมเพื่อใช้คำสั่ง 64 บิตเพื่อให้เร็วกว่าการแบ่งปกติ

#include <stdio.h>
#include <chrono>

static const unsigned s_bc = 32;
static const unsigned long long s_p = 1ULL << s_bc;
static const unsigned long long s_hp = s_p / 2;

static unsigned long long s_f;
static unsigned long long s_fr;

static void fastDivInitialize(const unsigned d)
{
    s_f = s_p / d;
    s_fr = s_f * (s_p - (s_f * d));
}

static unsigned fastDiv(const unsigned n)
{
    return (s_f * n + ((s_fr * n + s_hp) >> s_bc)) >> s_bc;
}

static bool fastDivCheck(const unsigned n, const unsigned d)
{
    // 32 to 64 cycles latency on modern cpus
    const unsigned expected = n / d;

    // At least 10 cycles latency on modern cpus
    const unsigned result = fastDiv(n);

    if (result != expected)
    {
        printf("Failed for: %u/%u != %u\n", n, d, expected);
        return false;
    }

    return true;
}

int main()
{
    unsigned result = 0;

    // Make sure to verify it works for your expected set of inputs
    const unsigned MAX_N = 65535;
    const unsigned MAX_D = 40000;

    const double ONE_SECOND_COUNT = 1000000000.0;

    auto t0 = std::chrono::steady_clock::now();
    unsigned count = 0;
    printf("Verifying...\n");
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            count += !fastDivCheck(n, d);
        }
    }
    auto t1 = std::chrono::steady_clock::now();
    printf("Errors: %u / %u (%.4fs)\n", count, MAX_D * (MAX_N + 1), (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += fastDiv(n);
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Fast division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    count = 0;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += n / d;
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Normal division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    getchar();
    return result;
}

0

ฉันคิดว่าในกรณีเดียวที่คุณต้องการคูณหรือหารด้วยกำลังสองคุณจะไม่ผิดกับการใช้ตัวดำเนินการ bitshift แม้ว่าคอมไพเลอร์จะแปลงให้เป็น MUL / DIV เพราะไมโครโปรเซสเซอร์ตัวประมวลผลบางตัว มาโคร) อย่างไรก็ตามสำหรับกรณีเหล่านี้คุณจะได้รับการปรับปรุงโดยเฉพาะอย่างยิ่งหากการเปลี่ยนแปลงมีค่ามากกว่า 1 หรือชัดเจนกว่านั้นถ้า CPU ไม่มีตัวดำเนินการ bithift มันจะเป็น MUL / DIV ต่อไป แต่ถ้า CPU มี ตัวดำเนินการ bithift คุณหลีกเลี่ยงสาขาไมโครโค้ดและนี่เป็นคำแนะนำน้อย

ฉันกำลังเขียนโค้ดบางตอนที่ต้องใช้การดำเนินการเพิ่มขึ้น / ลดลงเป็นจำนวนมากเพราะมันทำงานบนต้นไม้ไบนารีที่หนาแน่นและมีการดำเนินการอีกหนึ่งอย่างที่ฉันสงสัยว่าอาจเหมาะสมกว่าการเพิ่ม - ซ้าย (พลังของสองคูณ ) เปลี่ยนด้วยการเพิ่ม สิ่งนี้สามารถถูกแทนที่ด้วยกะซ้ายและ xor ถ้ากะกว้างกว่าจำนวนบิตที่คุณต้องการเพิ่มตัวอย่างง่ายๆคือ (i << 1) ^ 1 ซึ่งเพิ่มหนึ่งเป็นสองเท่าของค่า สิ่งนี้ไม่ได้นำไปใช้กับการเปลี่ยนแปลงที่ถูกต้อง (พลังของการหารสองครั้ง) เพราะการเปลี่ยนแปลงทางซ้าย (endian น้อย) เพียงเติมช่องว่างด้วยศูนย์

ในรหัสของฉันการทวีคูณ / หารด้วยสองและพลังของการดำเนินการสองอย่างนั้นมีการใช้อย่างเข้มข้นและเนื่องจากสูตรนั้นค่อนข้างสั้นอยู่แล้วคำสั่งต่าง ๆ ที่สามารถกำจัดได้นั้นจะได้รับอย่างมาก หากตัวประมวลผลไม่รองรับตัวดำเนินการ bithift เหล่านี้จะไม่เกิดการเพิ่มขึ้น แต่จะไม่มีการสูญเสีย

นอกจากนี้ในอัลกอริทึมที่ฉันเขียนพวกเขามองเห็นการเคลื่อนไหวที่เกิดขึ้นดังนั้นในแง่ที่ว่าพวกเขามีความชัดเจนมากขึ้น ด้านซ้ายของต้นไม้ไบนารีใหญ่กว่าและด้านขวามีขนาดเล็กลง ในรหัสของฉันเลขคี่และเลขคู่นั้นมีความสำคัญเป็นพิเศษและเด็กทางซ้ายมือทุกคนในต้นไม้เป็นเลขคี่และเด็กที่ถนัดขวาและรากแม้กระทั่ง ในบางกรณีที่ฉันยังไม่เคยพบมาก่อน แต่จริงๆแล้วฉันไม่เคยคิดเลยว่า x & 1 อาจเป็นการทำงานที่ดีที่สุดเมื่อเทียบกับ x% 2 x & 1 บนเลขคู่จะให้ศูนย์ แต่จะสร้าง 1 สำหรับเลขคี่

จะไกลเกินกว่าที่จะเป็นเลขคี่ / คู่ถ้าฉันได้ศูนย์สำหรับ x & 3 ฉันรู้ว่า 4 เป็นปัจจัยของจำนวนของเราและเหมือนกันสำหรับ x% 7 สำหรับ 8 และอื่น ๆ ฉันรู้ว่ากรณีเหล่านี้อาจมียูทิลิตี้ที่ จำกัด แต่ก็เป็นเรื่องดีที่ได้ทราบว่าคุณสามารถหลีกเลี่ยงการดำเนินการโมดูลัสและใช้การดำเนินการตรรกะระดับบิตแทนได้เนื่องจากการดำเนินการระดับบิตเกือบจะเร็วที่สุดเสมอ

ฉันประดิษฐ์สนามไบนารีที่หนาแน่นมากฉันจึงคาดหวังว่าผู้คนอาจไม่เข้าใจคุณค่าของความคิดเห็นนี้เนื่องจากผู้คนไม่ค่อยต้องการเพียงแค่ทำการแยกตัวประกอบเฉพาะบนพลังของสองหรือเพียงคูณ / หารกำลังสอง



0

หากคุณเปรียบเทียบผลลัพธ์สำหรับ x + x, x * 2 และ x << 1 ไวยากรณ์ในคอมไพเลอร์ gcc คุณจะได้ผลลัพธ์เดียวกันในแอสเซมบลี x86: https://godbolt.org/z/JLpp0j

        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        add     eax, eax
        pop     rbp
        ret

ดังนั้นคุณสามารถพิจารณาว่า gcc ฉลาดพอที่จะกำหนดทางออกที่ดีที่สุดของเขาได้อย่างอิสระจากสิ่งที่คุณพิมพ์


0

ฉันก็อยากจะดูว่าฉันสามารถเอาชนะบ้าน นี่คือ bitwise ที่ทั่วไปสำหรับตัวเลขใด ๆ โดยการคูณตัวเลขใด ๆ มาโครที่ฉันทำนั้นช้ากว่าการคูณ * ปกติประมาณ 25% อย่างที่คนอื่นพูดถ้ามันใกล้เคียงกับทวีคูณของ 2 หรือคูณทวีคูณของ 2 คุณอาจชนะ เช่น X * 23 ประกอบด้วย (X << 4) + (X << 2) + (X << 1) + X จะช้าลงแล้ว X * 65 ประกอบด้วย (X << 6) + X

#include <stdio.h>
#include <time.h>

#define MULTIPLYINTBYMINUS(X,Y) (-((X >> 30) & 1)&(Y<<30))+(-((X >> 29) & 1)&(Y<<29))+(-((X >> 28) & 1)&(Y<<28))+(-((X >> 27) & 1)&(Y<<27))+(-((X >> 26) & 1)&(Y<<26))+(-((X >> 25) & 1)&(Y<<25))+(-((X >> 24) & 1)&(Y<<24))+(-((X >> 23) & 1)&(Y<<23))+(-((X >> 22) & 1)&(Y<<22))+(-((X >> 21) & 1)&(Y<<21))+(-((X >> 20) & 1)&(Y<<20))+(-((X >> 19) & 1)&(Y<<19))+(-((X >> 18) & 1)&(Y<<18))+(-((X >> 17) & 1)&(Y<<17))+(-((X >> 16) & 1)&(Y<<16))+(-((X >> 15) & 1)&(Y<<15))+(-((X >> 14) & 1)&(Y<<14))+(-((X >> 13) & 1)&(Y<<13))+(-((X >> 12) & 1)&(Y<<12))+(-((X >> 11) & 1)&(Y<<11))+(-((X >> 10) & 1)&(Y<<10))+(-((X >> 9) & 1)&(Y<<9))+(-((X >> 8) & 1)&(Y<<8))+(-((X >> 7) & 1)&(Y<<7))+(-((X >> 6) & 1)&(Y<<6))+(-((X >> 5) & 1)&(Y<<5))+(-((X >> 4) & 1)&(Y<<4))+(-((X >> 3) & 1)&(Y<<3))+(-((X >> 2) & 1)&(Y<<2))+(-((X >> 1) & 1)&(Y<<1))+(-((X >> 0) & 1)&(Y<<0))
#define MULTIPLYINTBYSHIFT(X,Y) (((((X >> 30) & 1)<<31)>>31)&(Y<<30))+(((((X >> 29) & 1)<<31)>>31)&(Y<<29))+(((((X >> 28) & 1)<<31)>>31)&(Y<<28))+(((((X >> 27) & 1)<<31)>>31)&(Y<<27))+(((((X >> 26) & 1)<<31)>>31)&(Y<<26))+(((((X >> 25) & 1)<<31)>>31)&(Y<<25))+(((((X >> 24) & 1)<<31)>>31)&(Y<<24))+(((((X >> 23) & 1)<<31)>>31)&(Y<<23))+(((((X >> 22) & 1)<<31)>>31)&(Y<<22))+(((((X >> 21) & 1)<<31)>>31)&(Y<<21))+(((((X >> 20) & 1)<<31)>>31)&(Y<<20))+(((((X >> 19) & 1)<<31)>>31)&(Y<<19))+(((((X >> 18) & 1)<<31)>>31)&(Y<<18))+(((((X >> 17) & 1)<<31)>>31)&(Y<<17))+(((((X >> 16) & 1)<<31)>>31)&(Y<<16))+(((((X >> 15) & 1)<<31)>>31)&(Y<<15))+(((((X >> 14) & 1)<<31)>>31)&(Y<<14))+(((((X >> 13) & 1)<<31)>>31)&(Y<<13))+(((((X >> 12) & 1)<<31)>>31)&(Y<<12))+(((((X >> 11) & 1)<<31)>>31)&(Y<<11))+(((((X >> 10) & 1)<<31)>>31)&(Y<<10))+(((((X >> 9) & 1)<<31)>>31)&(Y<<9))+(((((X >> 8) & 1)<<31)>>31)&(Y<<8))+(((((X >> 7) & 1)<<31)>>31)&(Y<<7))+(((((X >> 6) & 1)<<31)>>31)&(Y<<6))+(((((X >> 5) & 1)<<31)>>31)&(Y<<5))+(((((X >> 4) & 1)<<31)>>31)&(Y<<4))+(((((X >> 3) & 1)<<31)>>31)&(Y<<3))+(((((X >> 2) & 1)<<31)>>31)&(Y<<2))+(((((X >> 1) & 1)<<31)>>31)&(Y<<1))+(((((X >> 0) & 1)<<31)>>31)&(Y<<0))
int main()
{
    int randomnumber=23;
    int randomnumber2=23;
    int checknum=23;
    clock_t start, diff;
    srand(time(0));
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYMINUS(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    int msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYMINUS Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYSHIFT(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYSHIFT Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum= randomnumber*randomnumber2;
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("normal * Time %d milliseconds", msec);
    return 0;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.