ข้อใดมีประสิทธิภาพมากขึ้นสำหรับแต่ละลูปหรือตัววนซ้ำ


206

วิธีใดมีประสิทธิภาพมากที่สุดในการสำรวจชุดสะสม

List<Integer>  a = new ArrayList<Integer>();
for (Integer integer : a) {
  integer.toString();
}

หรือ

List<Integer>  a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();) {
   Integer integer = (Integer) iterator.next();
   integer.toString();
}

โปรดทราบว่านี้ไม่ได้เป็นที่แน่นอนซ้ำของนี้ , นี้ , นี้หรือนี้แม้ว่าหนึ่งในคำตอบของคำถามที่ผ่านมาใกล้เคียง เหตุผลที่นี่ไม่ใช่การล่อลวงเพราะส่วนใหญ่เป็นการเปรียบเทียบลูปที่คุณโทรget(i)ภายในลูปแทนที่จะใช้ตัววนซ้ำ

ตามที่แนะนำในMetaฉันจะโพสต์คำตอบของคำถามนี้


ฉันคิดว่ามันไม่ได้สร้างความแตกต่างเนื่องจาก Java และกลไกการสร้าง
Hassan Syed

ซ้ำที่อาจเกิดขึ้น: stackoverflow.com/questions/89891/…
OMG Ponies

2
@OMG Ponies: ฉันไม่เชื่อว่านี่เป็นสิ่งที่ซ้ำซ้อนเพราะมันไม่ได้เปรียบเทียบลูปกับตัววนซ้ำ แต่ถามว่าทำไมคอลเลกชันถึงส่งคืนตัววนซ้ำแทนที่จะมีตัววนซ้ำโดยตรงในชั้นเรียน
พอล Wagland

คำตอบ:


264

หากคุณเพียงแค่เดินไปที่คอลเลกชันเพื่ออ่านค่าทั้งหมดก็ไม่มีความแตกต่างระหว่างการใช้ตัววนซ้ำหรือการวนซ้ำแบบวนซ้ำเนื่องจากการวนรอบใหม่นั้นใช้ตัววนซ้ำใต้น้ำ

อย่างไรก็ตามหากคุณหมายถึงการวนลูปแบบ "c-style" แบบเก่า:

for(int i=0; i<list.size(); i++) {
   Object o = list.get(i);
}

จากนั้นลูปใหม่หรือตัววนซ้ำอาจมีประสิทธิภาพมากขึ้นโดยขึ้นอยู่กับโครงสร้างข้อมูลพื้นฐาน เหตุผลสำหรับสิ่งนี้คือสำหรับโครงสร้างข้อมูลบางอย่างget(i)เป็นการดำเนินการ O (n) ซึ่งทำให้การวนซ้ำเป็นการดำเนินการ O (n 2 ) รายการที่เชื่อมโยงแบบดั้งเดิมเป็นตัวอย่างของโครงสร้างข้อมูลดังกล่าว ตัววนซ้ำทั้งหมดมีข้อกำหนดพื้นฐานที่next()ควรใช้ในการดำเนินการ O (1) ทำให้วนซ้ำ O (n)

ในการตรวจสอบว่าตัววนซ้ำใช้ใต้น้ำสำหรับไวยากรณ์ลูปใหม่ให้เปรียบเทียบไบต์ที่สร้างขึ้นจากตัวอย่างโค้ด Java ต่อไปนี้ ก่อนอื่นสำหรับ for loop:

List<Integer>  a = new ArrayList<Integer>();
for (Integer integer : a)
{
  integer.toString();
}
// Byte code
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
 ASTORE 3
 GOTO L2
L3
 ALOAD 3
 INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
 CHECKCAST java/lang/Integer
 ASTORE 2 
 ALOAD 2
 INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
 POP
L2
 ALOAD 3
 INVOKEINTERFACE java/util/Iterator.hasNext()Z
 IFNE L3

และประการที่สองตัววนซ้ำ:

List<Integer>  a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();)
{
  Integer integer = (Integer) iterator.next();
  integer.toString();
}
// Bytecode:
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
 ASTORE 2
 GOTO L7
L8
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
 CHECKCAST java/lang/Integer
 ASTORE 3
 ALOAD 3
 INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
 POP
L7
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.hasNext()Z
 IFNE L8

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


4
ฉันเชื่อว่าเขาพูดตรงข้ามนั่นคือ foo.get (i) มีประสิทธิภาพน้อยกว่ามาก คิดถึง LinkedList หากคุณทำ foo.get (i) ตรงกลางของ LinkedList จะต้องข้ามโหนดก่อนหน้านี้ทั้งหมดเพื่อไปที่ i ในทางกลับกันตัววนซ้ำจะคอยจัดการกับโครงสร้างข้อมูลพื้นฐานและจะช่วยให้คุณสามารถเดินข้ามโหนดทีละโหนด
Michael Krauklis

1
ไม่ใช่เรื่องใหญ่ แต่for(int i; i < list.size(); i++) {สไตล์วนยังต้องประเมินlist.size()ในตอนท้ายของการทำซ้ำแต่ละครั้งถ้ามีการใช้มันบางครั้งมีประสิทธิภาพมากขึ้นในการแคชผลลัพธ์ของlist.size()ครั้งแรก
Brett Ryan

3
ที่จริงแล้วข้อความต้นฉบับนั้นเป็นจริงสำหรับกรณีของ ArrayList และอื่น ๆ ทั้งหมดที่ใช้อินเทอร์เฟซ RandomAccess การวนซ้ำ "C-style" เร็วกว่าการวนซ้ำตาม Iterator docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
andresp

4
เหตุผลหนึ่งที่ใช้วงวน C สไตล์เก่าแทนที่จะใช้วิธี Iterator ไม่ว่าจะเป็นรุ่น foreach หรือรุ่น desugar'd จะเป็นแบบขยะก็ตาม โครงสร้างข้อมูลจำนวนมากยกตัวอย่าง Iterator ใหม่เมื่อ .iterator () ถูกเรียกใช้อย่างไรก็ตามพวกเขาสามารถเข้าถึงได้โดยไม่ต้องจัดสรรโดยใช้ลูป C-style สิ่งนี้อาจมีความสำคัญในสภาพแวดล้อมที่มีประสิทธิภาพสูงซึ่งผู้ใช้พยายามหลีกเลี่ยง (a) กดปุ่มตัวจัดสรรหรือ (b) การรวบรวมขยะ
Dan

3
เช่นเดียวกับความคิดเห็นอื่นสำหรับ ArrayLists ห่วงสำหรับ (int i = 0 .... ) นั้นเร็วกว่าการใช้ตัววนซ้ำประมาณ 2x หรือวิธีการสำหรับ (:) ดังนั้นมันจึงขึ้นอยู่กับโครงสร้างพื้นฐานจริง ๆ และในฐานะที่เป็นข้อความด้านข้างการทำซ้ำ HashSets ก็มีราคาแพงมาก (มากกว่ารายการ Array) ดังนั้นจงหลีกเลี่ยงสิ่งที่เหมือนกาฬโรค (ถ้าทำได้)
ลีโอ

106

ความแตกต่างไม่ได้อยู่ที่ประสิทธิภาพ แต่เป็นความสามารถ เมื่อใช้การอ้างอิงโดยตรงคุณจะมีอำนาจมากกว่าอย่างชัดเจนโดยใช้ชนิดตัววนซ้ำ (เช่น List.iterator () กับ List.listIterator () แม้ว่าโดยส่วนใหญ่แล้วพวกเขากลับมาใช้งานแบบเดียวกัน) คุณยังมีความสามารถในการอ้างอิง Iterator ในวงของคุณ สิ่งนี้ช่วยให้คุณทำสิ่งต่าง ๆ เช่นลบรายการออกจากคอลเลกชันของคุณโดยไม่ได้รับ ConcurrentModificationException

เช่น

ไม่เป็นไร

Set<Object> set = new HashSet<Object>();
// add some items to the set

Iterator<Object> setIterator = set.iterator();
while(setIterator.hasNext()){
     Object o = setIterator.next();
     if(o meets some condition){
          setIterator.remove();
     }
}

นี่ไม่ใช่เนื่องจากจะทำให้เกิดข้อยกเว้นการแก้ไขพร้อมกัน:

Set<Object> set = new HashSet<Object>();
// add some items to the set

for(Object o : set){
     if(o meets some condition){
          set.remove(o);
     }
}

12
นี่เป็นเรื่องจริงแม้ว่ามันจะไม่ตอบคำถามที่ฉันให้ไว้โดยตรงเพื่อให้ข้อมูล +1 และตอบคำถามเชิงตรรกะตามมา
พอล Wagland

1
ใช่เราสามารถเข้าถึงองค์ประกอบคอลเลกชันด้วย foreach loop แต่เราไม่สามารถลบองค์ประกอบเหล่านั้น แต่เราสามารถลบองค์ประกอบด้วย Iterator
Akash5288

22

เพื่อขยายคำตอบของเปาโลเขาได้แสดงให้เห็นว่า bytecode นั้นเหมือนกันกับคอมไพเลอร์นั้น (น่าจะเป็น javac ของ Sun?) แต่คอมไพเลอร์ต่าง ๆ ไม่รับประกันว่าจะสร้าง bytecode เดียวกันใช่ไหม? หากต้องการดูความแตกต่างที่แท้จริงระหว่างสองสิ่งนี้ให้ตรงไปที่แหล่งข้อมูลและตรวจสอบ Java Language Specification โดยเฉพาะ14.14.2 "The Enhanced for statement" :

forคำสั่งที่ปรับปรุงแล้วเทียบเท่ากับforคำสั่งพื้นฐานของแบบฟอร์ม:

for (I #i = Expression.iterator(); #i.hasNext(); ) {
    VariableModifiers(opt) Type Identifier = #i.next();    
    Statement 
}

กล่าวอีกนัยหนึ่งคือ JLS ต้องการให้ทั้งสองสิ่งนั้นเทียบเท่ากัน ในทางทฤษฎีที่อาจหมายถึงความแตกต่างเล็กน้อยใน bytecode แต่ในความเป็นจริงการปรับปรุงสำหรับวงจะต้อง:

  • เรียกใช้.iterator()เมธอด
  • ใช้ .hasNext()
  • ทำให้ตัวแปรท้องถิ่นพร้อมใช้งานผ่าน .next()

ดังนั้นในคำอื่น ๆ เพื่อวัตถุประสงค์ในทางปฏิบัติ bytecode จะเหมือนกันหรือเกือบจะเหมือนกัน เป็นการยากที่จะมองเห็นการใช้งานคอมไพเลอร์ซึ่งจะส่งผลให้เกิดความแตกต่างอย่างมีนัยสำคัญระหว่างทั้งสอง


ที่จริงแล้วการทดสอบที่ฉันทำคือคอมไพเลอร์ Eclipse แต่จุดทั่วไปของคุณยังคงอยู่ +1
Paul Wagland

3

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

for (Iterator<CustomObj> iter = customList.iterator(); iter.hasNext()){
   CustomObj custObj = iter.next();
   ....
}

ปัญหาด้านประสิทธิภาพของการวนซ้ำที่ใช้ตัววนซ้ำเนื่องจากเป็น:

  1. การจัดสรรวัตถุแม้ว่ารายการว่างเปล่า ( Iterator<CustomObj> iter = customList.iterator(););
  2. iter.hasNext() ระหว่างการวนซ้ำทุกครั้งของการวนรอบจะมีการเรียกใช้เสมือน invokeInterface (ผ่านคลาสทั้งหมดจากนั้นทำการค้นหาตารางวิธีก่อนการกระโดด)
  3. การใช้ตัววนซ้ำต้องดำเนินการค้นหาอย่างน้อย 2 ฟิลด์เพื่อให้hasNext()ตัวเลขการโทรมีค่า: # 1 รับจำนวนนับปัจจุบันและ # 2 ได้รับจำนวนทั้งหมด
  4. ภายใน body loop มีการเรียกใช้เสมือนเรียกใช้อินเทอร์เฟซแบบอินเทอร์เฟซอีกครั้งiter.next(ดังนั้น: ไปที่คลาสทั้งหมดและทำการค้นหาตารางเมธอดก่อนที่จะข้าม) และเช่นเดียวกันต้องทำการค้นหาฟิลด์: # 1 รับดัชนีและ # 2 อาร์เรย์ที่จะทำการชดเชยเข้าไป (ในการวนซ้ำทุกครั้ง)

การปรับให้เหมาะสมที่อาจเกิดขึ้นคือการเปลี่ยนไปใช้index iterationกับการค้นหาขนาดแคช:

for(int x = 0, size = customList.size(); x < size; x++){
  CustomObj custObj = customList.get(x);
  ...
}

ที่นี่เรามี:

  1. หนึ่ง invokeInterface เรียกวิธีเสมือนcustomList.size()ในการสร้างเริ่มต้นของ for loop เพื่อให้ได้ขนาด
  2. การเรียกเมธอด get customList.get(x)ระหว่าง body สำหรับลูปซึ่งเป็นการค้นหาฟิลด์ไปยังอาเรย์แล้วสามารถออฟเซ็ตลงในอาเรย์

เราลดการเรียกใช้เมธอดจำนวนมากการค้นหาฟิลด์ สิ่งนี้คุณไม่ต้องการทำกับLinkedListสิ่งที่ไม่ใช่RandomAccessคอลเลกชัน obj มิฉะนั้นสิ่งcustomList.get(x)นั้นจะกลายเป็นสิ่งที่ต้องเข้าไปสำรวจLinkedListทุก ๆ รอบ

นี่คือที่สมบูรณ์แบบเมื่อคุณรู้ว่าเป็นRandomAccessคอลเลกชันรายการตาม


1

foreachใช้ iterators ใต้ฝากระโปรงต่อไป จริงๆแล้วมันเป็นแค่น้ำตาลซินแทคติค

พิจารณาโปรแกรมต่อไปนี้:

import java.util.List;
import java.util.ArrayList;

public class Whatever {
    private final List<Integer> list = new ArrayList<>();
    public void main() {
        for(Integer i : list) {
        }
    }
}

ลองมารวบรวมไว้ด้วยjavac Whatever.java,
และอ่าน bytecode ถอดชิ้นส่วนของmain()การใช้javap -c Whatever:

public void main();
  Code:
     0: aload_0
     1: getfield      #4                  // Field list:Ljava/util/List;
     4: invokeinterface #5,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
     9: astore_1
    10: aload_1
    11: invokeinterface #6,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
    16: ifeq          32
    19: aload_1
    20: invokeinterface #7,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
    25: checkcast     #8                  // class java/lang/Integer
    28: astore_2
    29: goto          10
    32: return

เราจะเห็นว่าforeachคอมไพล์ลงในโปรแกรมซึ่ง:

  • สร้างตัววนซ้ำโดยใช้ List.iterator()
  • หากIterator.hasNext(): เรียกใช้Iterator.next()และวนรอบต่อไป

ในฐานะที่เป็น "ทำไมไม่ห่วงไร้ประโยชน์นี้ได้รับการปรับให้เหมาะสมออกจากรหัสเรียบเรียงเราจะเห็นว่ามันไม่ได้ทำอะไรกับรายการรายการ": ดีก็เป็นไปได้สำหรับคุณที่จะรหัส iterable ดังกล่าวของคุณที่.iterator()มีผลข้างเคียง หรือดังนั้นจึง.hasNext()มีผลข้างเคียงหรือผลที่ตามมาอย่างมีความหมาย

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

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

ดีที่สุดที่เราจะหวังจะเป็นคอมไพเลอร์เตือน เป็นเรื่องที่น่าสนใจที่javac -Xlint:all Whatever.javaไม่ได้เตือนเราเกี่ยวกับเนื้อความที่ว่างเปล่านี้ IntelliJ IDEA ทำเช่นนั้น เป็นที่ยอมรับว่าฉันได้กำหนดค่า IntelliJ ให้ใช้ Eclipse Compiler แต่นั่นอาจไม่ใช่สาเหตุ

ป้อนคำอธิบายรูปภาพที่นี่


0

Iterator เป็นอินเตอร์เฟสในเฟรมเวิร์ก Java Collections ที่จัดเตรียมเมธอดเพื่อข้ามหรือทำซ้ำบนคอลเล็กชัน

ทั้งตัววนซ้ำและวนซ้ำจะทำหน้าที่คล้ายกันเมื่อแรงจูงใจของคุณคือเพียงข้ามผ่านคอลเล็กชันเพื่ออ่านองค์ประกอบ

for-each เป็นเพียงวิธีเดียวในการทำซ้ำในคอลเล็กชัน

ตัวอย่างเช่น:

List<String> messages= new ArrayList<>();

//using for-each loop
for(String msg: messages){
    System.out.println(msg);
}

//using iterator 
Iterator<String> it = messages.iterator();
while(it.hasNext()){
    String msg = it.next();
    System.out.println(msg);
}

และสำหรับแต่ละลูปสามารถใช้กับวัตถุที่ใช้อินเตอร์เฟสตัววนซ้ำเท่านั้น

ตอนนี้กลับไปที่กรณีของการวนซ้ำและวนซ้ำ

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

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


1
คำสั่งของคุณเกี่ยวกับความแตกต่างไม่เป็นความจริงสำหรับแต่ละลูปยังใช้ตัววนซ้ำใต้น้ำและมีพฤติกรรมแบบเดียวกัน
Paul Wagland

@Pault Wagland, ฉันได้แก้ไขคำตอบแล้วขอบคุณที่ชี้ให้เห็นข้อผิดพลาด
eccentricCoder

การอัปเดตของคุณยังไม่ถูกต้อง โค้ดสองชุดที่คุณกำหนดโดยภาษาให้เหมือนกัน หากมีความแตกต่างในพฤติกรรมที่เป็นข้อบกพร่องในการใช้งาน ข้อแตกต่างเพียงอย่างเดียวคือคุณสามารถเข้าถึงตัววนซ้ำได้หรือไม่
Paul Wagland

@Paul Wagland แม้ว่าคุณจะใช้การใช้งานเริ่มต้นของแต่ละลูปที่ใช้ตัววนซ้ำมันจะยังคงใช้งานได้และจะมีข้อยกเว้นหากคุณลองใช้เมธอด remove () ระหว่างการดำเนินการพร้อมกัน ชำระเงินต่อไปนี้สำหรับข้อมูลเพิ่มเติมที่นี่
eccentricCoder

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

-8

เราควรหลีกเลี่ยงการใช้แบบดั้งเดิมเป็นวงในขณะที่ทำงานกับ Collections เหตุผลง่ายๆที่ฉันจะให้คือความซับซ้อนของ for loop นั้นเป็นไปตามลำดับ O (sqr (n)) และความซับซ้อนของ Iterator หรือแม้แต่การปรับปรุงสำหรับ loop นั้นก็แค่ O (n) ดังนั้นมันจึงให้ความแตกต่างของการแสดง .. แค่จดรายการ 1,000 รายการแล้วพิมพ์โดยใช้ทั้งสองวิธี และพิมพ์ความแตกต่างของเวลาสำหรับการดำเนินการ คุณสามารถเห็นความแตกต่าง


โปรดเพิ่มตัวอย่างที่เป็นตัวอย่างเพื่อสนับสนุนข้อความของคุณ
Rajesh Pitty

@Chandan ขออภัยที่คุณเขียนไม่ถูกต้อง ตัวอย่างเช่น: std :: vector เป็นชุดสะสม แต่ค่าใช้จ่ายในการเข้าถึง O (1) ดังนั้นแบบดั้งเดิมสำหรับการวนรอบเวกเตอร์จึงเป็นเพียง O (n) ฉันคิดว่าคุณต้องการพูดว่าถ้าการเข้าถึงของ underlaying container มีค่าใช้จ่ายการเข้าถึงของ O (n) ดังนั้นมันจึงเป็นรายการ std :: กว่าความซับซ้อนของ O (n ^ 2) การใช้ตัววนซ้ำในกรณีนั้นจะลดต้นทุนเป็น O (n) เนื่องจากตัววนซ้ำอนุญาตการเข้าถึงองค์ประกอบโดยตรง
ไกเซอร์

หากคุณทำการคำนวณผลต่างเวลาตรวจสอบให้แน่ใจว่าทั้งสองชุดนั้นถูกจัดเรียง (หรือกระจายแบบสุ่มค่อนข้างไม่มีการเรียงลำดับ) และรันการทดสอบสองครั้งสำหรับแต่ละชุดและคำนวณการวิ่งครั้งที่สองของแต่ละชุดเท่านั้น ตรวจสอบเวลาของคุณอีกครั้งด้วยสิ่งนี้ (เป็นคำอธิบายที่ยาวว่าทำไมคุณต้องทำการทดสอบสองครั้ง) คุณต้องแสดง (อาจมีรหัส) วิธีนี้เป็นจริง ไม่อย่างนั้นเท่าที่ฉันเคยรู้จักทั้งสองก็เหมือนกันในแง่ของประสิทธิภาพ แต่ไม่ใช่ความสามารถ
ydobonebi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.