Thay thế / tối ưu hóa hiệu suất Java HashMap


102

Tôi muốn tạo một HashMap lớn nhưng put()hiệu suất không đủ tốt. Bất kỳ ý tưởng?

Các đề xuất về cấu trúc dữ liệu khác được hoan nghênh nhưng tôi cần tính năng tra cứu của Bản đồ Java:

map.get(key)

Trong trường hợp của tôi, tôi muốn tạo một bản đồ với 26 triệu mục nhập. Sử dụng Java HashMap tiêu chuẩn, tốc độ đặt trở nên chậm không thể chịu nổi sau 2-3 triệu lần chèn.

Ngoài ra, có ai biết nếu sử dụng các bản phân phối mã băm khác nhau cho các khóa có thể giúp được không?

Phương thức mã băm của tôi:

byte[] a = new byte[2];
byte[] b = new byte[3];
...

public int hashCode() {
    int hash = 503;
    hash = hash * 5381 + (a[0] + a[1]);
    hash = hash * 5381 + (b[0] + b[1] + b[2]);
    return hash;
}

Tôi đang sử dụng thuộc tính kết hợp của phép cộng để đảm bảo rằng các đối tượng bằng nhau có cùng một mã băm. Mảng là các byte có giá trị trong khoảng 0 - 51. Giá trị chỉ được sử dụng một lần trong một trong hai mảng. Các đối tượng bằng nhau nếu các mảng a chứa các giá trị giống nhau (theo một trong hai thứ tự) và tương tự đối với mảng b. Vậy a = {0,1} b = {45,12,33} và a = {1,0} b = {33,45,12} bằng nhau.

CHỈNH SỬA, một số lưu ý:

  • Một số người đã chỉ trích việc sử dụng bản đồ băm hoặc cấu trúc dữ liệu khác để lưu trữ 26 triệu mục nhập. Tôi không thể hiểu tại sao điều này có vẻ kỳ lạ. Với tôi, nó giống như một vấn đề về cấu trúc dữ liệu và thuật toán cổ điển. Tôi có 26 triệu mục và tôi muốn có thể nhanh chóng chèn chúng vào và tra cứu chúng từ cấu trúc dữ liệu: cung cấp cho tôi cấu trúc dữ liệu và thuật toán.

  • Đặt dung lượng ban đầu của Java HashMap mặc định thành 26 triệu sẽ làm giảm hiệu suất.

  • Một số người đã đề xuất sử dụng cơ sở dữ liệu, trong một số trường hợp khác, đó chắc chắn là lựa chọn thông minh. Nhưng tôi thực sự đang hỏi một câu hỏi về cấu trúc dữ liệu và thuật toán, một cơ sở dữ liệu đầy đủ sẽ quá mức cần thiết và chậm hơn nhiều so với một giải pháp cấu trúc cơ sở dữ liệu tốt (xét cho cùng thì cơ sở dữ liệu chỉ là phần mềm nhưng sẽ có giao tiếp và có thể là chi phí đĩa).


29
Nếu HashMap trở nên chậm chạp, rất có thể hàm băm của bạn không đủ tốt.
Pascal Cuoq

12
bác sĩ, một điều đau khổ khi tôi làm này
skaffman

12
Đây thực sự là một câu hỏi tốt; một cuộc biểu tình tốt đẹp của lý do tại sao các thuật toán băm quan trọng và ảnh hưởng đến những gì họ có thể có hiệu suất
oxbow_lakes

12
Tổng của a có phạm vi từ 0 đến 102 và tổng của b có phạm vi từ 0 đến 153, vì vậy bạn chỉ có 15.606 giá trị băm có thể có và trung bình là 1.666 khóa có cùng Mã băm. Bạn nên thay đổi mã băm của mình để số mã băm có thể có nhiều hơn số khóa.
Peter Lawrey

6
Tôi đã xác định bằng tâm lý rằng bạn đang lập mô hình Texas Hold 'Em Poker ;-)
bacar 23/12/12

Câu trả lời:


56

Như nhiều người đã chỉ ra hashCode()phương pháp này là đáng trách. Nó chỉ tạo ra khoảng 20.000 mã cho 26 triệu đối tượng riêng biệt. Đó là mức trung bình của 1.300 đối tượng trên mỗi thùng băm = rất tệ. Tuy nhiên, nếu tôi chuyển hai mảng thành một số trong cơ số 52, tôi đảm bảo nhận được mã băm duy nhất cho mọi đối tượng:

public int hashCode() {       
    // assume that both a and b are sorted       
    return a[0] + powerOf52(a[1], 1) + powerOf52(b[0], 2) + powerOf52(b[1], 3) + powerOf52(b[2], 4);
}

public static int powerOf52(byte b, int power) {
    int result = b;
    for (int i = 0; i < power; i++) {
        result *= 52;
    }
    return result;
}

Các mảng được sắp xếp để đảm bảo các phương thức này thực hiện hashCode()hợp đồng rằng các đối tượng bằng nhau có cùng mã băm. Sử dụng phương pháp cũ, số lần đặt trung bình mỗi giây trên các khối 100.000 lần đặt, 100.000 đến 2.000.000 lần là:

168350.17
109409.195
81344.91
64319.023
53780.79
45931.258
39680.29
34972.676
31354.514
28343.062
25562.371
23850.695
22299.22
20998.006
19797.799
18702.951
17702.434
16832.182
16084.52
15353.083

Sử dụng phương pháp mới mang lại:

337837.84
337268.12
337078.66
336983.97
313873.2
317460.3
317748.5
320000.0
309704.06
310752.03
312944.5
265780.75
275540.5
264350.44
273522.97
270910.94
279008.7
276285.5
283455.16
289603.25

Tốt hơn nhiều. Phương pháp cũ kết thúc rất nhanh trong khi phương pháp mới vẫn giữ được thông lượng tốt.


17
Tôi đề nghị không sửa đổi các mảng trong hashCodephương thức. Theo quy ước, hashCodekhông thay đổi trạng thái của đối tượng. Có lẽ hàm tạo sẽ là nơi tốt hơn để sắp xếp chúng.
Michael Myers

Tôi đồng ý rằng việc sắp xếp các mảng sẽ xảy ra trong hàm tạo. Mã được hiển thị dường như không bao giờ đặt Mã băm. Tính toán mã có thể được thực hiện đơn giản như sau: int result = a[0]; result = result * 52 + a[1]; //etc.
rsp

Tôi đồng ý rằng sắp xếp trong hàm tạo và sau đó tính toán mã băm như mmyers và rsp đề xuất là tốt hơn. Trong trường hợp của tôi, giải pháp của tôi có thể chấp nhận được và tôi muốn làm nổi bật thực tế là các mảng phải được sắp xếp hashCode()để hoạt động.
nash

3
Lưu ý rằng bạn cũng có thể lưu mã băm vào bộ nhớ cache (và vô hiệu hóa một cách thích hợp nếu đối tượng của bạn có thể thay đổi được).
NateS

1
Chỉ cần sử dụng java.util.Arrays.hashCode () . Nó đơn giản hơn (không cần viết và tự duy trì mã), tính toán của nó có thể nhanh hơn (ít phép nhân hơn) và sự phân bố các mã băm của nó có thể sẽ đồng đều hơn.
jcsahnwaldt Phục hồi Monica

18

Một điều tôi nhận thấy trong hashCode()phương pháp của bạn là thứ tự của các phần tử trong mảng a[]b[]không quan trọng. Do đó (a[]={1,2,3}, b[]={99,100})sẽ băm thành giá trị tương tự như (a[]={3,1,2}, b[]={100,99}). Trên thực tế tất cả các phím k1k2nơi sum(k1.a)==sum(k2.a)sum(k1.b)=sum(k2.b)sẽ dẫn đến va chạm. Tôi khuyên bạn nên gán trọng số cho từng vị trí của mảng:

hash = hash * 5381 + (c0*a[0] + c1*a[1]);
hash = hash * 5381 + (c0*b[0] + c1*b[1] + c3*b[2]);

ở đâu, c0, c1c3riêng biệt hằng (bạn có thể sử dụng các hằng số khác nhau cho bnếu cần). Điều đó thậm chí sẽ ra nhiều thứ hơn một chút.


Mặc dù tôi cũng nên nói thêm rằng nó sẽ không hoạt động với tôi vì tôi muốn thuộc tính mà các mảng có cùng phần tử theo các thứ tự khác nhau cung cấp cùng một mã băm.
nash

5
Trong trường hợp đó, bạn có 52C2 + 52C3 mã băm (23426 theo máy tính của tôi), và một bản đồ băm rất nhiều là công cụ sai cho công việc.
kdgregory

Trên thực tế, điều này sẽ làm tăng hiệu suất. Càng nhiều va chạm thì càng ít mục nhập trong eq bảng băm. ít việc phải làm. Không phải là bảng băm (trông ổn) cũng không phải bảng băm (hoạt động tốt) Tôi cá rằng nó nằm trong quá trình tạo đối tượng, nơi hiệu suất đang giảm sút.
OscarRyz

7
@Oscar - nhiều va chạm hơn tương đương với nhiều việc phải làm hơn, vì bây giờ bạn phải thực hiện tìm kiếm tuyến tính chuỗi băm. Nếu bạn có 26.000.000 giá trị khác biệt trên mỗi dấu bằng () và 26.000 giá trị khác biệt trên mỗi Mã băm (), thì chuỗi thùng sẽ có 1.000 đối tượng mỗi.
kdgregory

@ Nash0: Có vẻ như bạn đang nói rằng bạn muốn các mã này có cùng Mã băm nhưng đồng thời không bằng nhau (như được định nghĩa bởi phương thức equals ()). Tại sao bạn sẽ muốn điều đó?
MAK

17

Nói rõ hơn về Pascal: Bạn có hiểu HashMap hoạt động như thế nào không? Bạn có một số vị trí trong bảng băm của mình. Giá trị băm cho mỗi khóa được tìm thấy, và sau đó được ánh xạ tới một mục nhập trong bảng. Nếu hai giá trị băm ánh xạ đến cùng một mục nhập - một "xung đột băm" - HashMap sẽ xây dựng một danh sách được liên kết.

Xung đột băm có thể giết chết hiệu suất của bản đồ băm. Trong trường hợp cực đoan, nếu tất cả các khóa của bạn có cùng một mã băm hoặc nếu chúng có các mã băm khác nhau nhưng tất cả đều ánh xạ đến cùng một vị trí, thì bản đồ băm của bạn sẽ biến thành một danh sách được liên kết.

Vì vậy, nếu bạn đang gặp vấn đề về hiệu suất, điều đầu tiên tôi sẽ kiểm tra là: Tôi có nhận được phân phối mã băm trông ngẫu nhiên không? Nếu không, bạn cần một hàm băm tốt hơn. Chà, "tốt hơn" trong trường hợp này có thể có nghĩa là "tốt hơn cho tập dữ liệu cụ thể của tôi". Giống như, giả sử bạn đang làm việc với các chuỗi và bạn đã lấy độ dài của chuỗi cho giá trị băm. (Không phải cách hoạt động của String.hashCode của Java, mà tôi chỉ đang tạo ra một ví dụ đơn giản.) Nếu các chuỗi của bạn có độ dài khác nhau, từ 1 đến 10.000 và được phân bổ khá đồng đều trên phạm vi đó, điều này có thể rất tốt. hàm băm. Nhưng nếu các chuỗi của bạn chỉ có 1 hoặc 2 ký tự, đây sẽ là một hàm băm rất tệ.

Chỉnh sửa: Tôi nên thêm: Mỗi khi bạn thêm một mục mới, HashMap sẽ kiểm tra xem đây có phải là bản sao không. Khi có xung đột băm, nó phải so sánh khóa đến với mọi khóa được ánh xạ tới vị trí đó. Vì vậy, trong trường hợp xấu nhất khi mọi thứ được băm thành một vị trí duy nhất, khóa thứ hai được so sánh với khóa đầu tiên, khóa thứ ba được so sánh với khóa # 1 và # 2, khóa thứ tư được so sánh với # 1, # 2 và # 3 , v.v. Khi bạn đạt đến khóa # 1 triệu, bạn đã thực hiện hơn một nghìn tỷ so sánh.

@Oscar: Umm, tôi không hiểu đó là "không thực sự". Nó giống như một "để tôi làm rõ". Nhưng có, đúng là nếu bạn tạo một mục nhập mới có cùng khóa với một mục nhập hiện có, thì điều này sẽ ghi đè mục nhập đầu tiên. Đó là ý của tôi khi nói về việc tìm kiếm các bản sao trong đoạn cuối: Bất cứ khi nào một khóa băm vào cùng một vị trí, HashMap phải kiểm tra xem đó có phải là bản sao của một khóa hiện có hay không, hay chúng chỉ nằm trong cùng một vị trí do trùng hợp hàm băm. Tôi không biết rằng đó là "toàn bộ điểm" của HashMap: Tôi có thể nói rằng "điểm toàn diện" là bạn có thể truy xuất các phần tử bằng khóa một cách nhanh chóng.

Nhưng dù sao, điều đó không ảnh hưởng đến "toàn bộ điểm" mà tôi đang cố gắng thực hiện: Khi bạn có hai chìa khóa - vâng, các chìa khóa khác nhau, không cùng một chìa khóa hiển thị lại - ánh xạ đến cùng một vị trí trong bảng , HashMap xây dựng một danh sách liên kết. Sau đó, vì nó phải kiểm tra từng khóa mới để xem liệu nó có thực sự là bản sao của khóa hiện có hay không, mỗi lần cố gắng thêm mục nhập mới ánh xạ vào cùng vị trí này phải theo đuổi danh sách liên kết kiểm tra từng mục nhập hiện có để xem điều này là bản sao của khóa đã thấy trước đó hoặc nếu đó là khóa mới.

Cập nhật rất lâu sau bài viết gốc

Tôi vừa nhận được một phiếu bầu cho câu trả lời này 6 năm sau khi đăng, điều này khiến tôi phải đọc lại câu hỏi.

Hàm băm được đưa ra trong câu hỏi không phải là hàm băm tốt cho 26 triệu mục nhập.

Nó cộng a [0] + a [1] và b [0] + b [1] + b [2] cùng nhau. Anh ấy nói giá trị của mỗi byte nằm trong khoảng từ 0 đến 51, do đó chỉ cung cấp (51 * 2 + 1) * (51 * 3 + 1) = 15.862 giá trị băm có thể có. Với 26 triệu mục nhập, điều này có nghĩa là trung bình có khoảng 1639 mục nhập cho mỗi giá trị băm. Đó là rất nhiều va chạm, đòi hỏi rất nhiều và rất nhiều tìm kiếm tuần tự thông qua các danh sách được liên kết.

OP nói rằng các thứ tự khác nhau trong mảng a và mảng b nên được coi là bằng nhau, tức là [[1,2], [3,4,5]]. Bằng ([[2,1], [5,3,4] ]), và do đó để thực hiện hợp đồng, họ phải có mã băm bằng nhau. Được chứ. Tuy nhiên, có rất nhiều hơn 15.000 giá trị có thể. Hàm băm được đề xuất thứ hai của anh ấy tốt hơn nhiều, cho phạm vi rộng hơn.

Mặc dù như một người khác đã nhận xét, có vẻ như không thích hợp cho một hàm băm để thay đổi dữ liệu khác. Sẽ có ý nghĩa hơn nếu "bình thường hóa" đối tượng khi nó được tạo hoặc để hàm băm hoạt động từ các bản sao của mảng. Ngoài ra, việc sử dụng một vòng lặp để tính toán các hằng số mỗi khi thông qua hàm là không hiệu quả. Vì chỉ có bốn giá trị ở đây, tôi sẽ viết

return a[0]+a[1]*52+b[0]*52*52+b[1]*52*52*52+b[2]*52*52*52*52;

điều này sẽ khiến trình biên dịch thực hiện phép tính một lần tại thời điểm biên dịch; hoặc có 4 hằng số tĩnh được xác định trong lớp.

Ngoài ra, bản nháp đầu tiên tại một hàm băm có một số phép tính không làm gì thêm vào phạm vi đầu ra. Lưu ý rằng trước tiên anh ta đặt băm = 503 hơn nhân với 5381 trước khi xem xét các giá trị từ lớp. Vì vậy, trên thực tế, anh ta thêm 503 * 5381 vào mọi giá trị. Điều này đạt được gì? Việc thêm một hằng số vào mọi giá trị băm chỉ đốt cháy các chu kỳ cpu mà không đạt được bất kỳ điều gì hữu ích. Bài học ở đây: Thêm độ phức tạp vào hàm băm không phải là mục tiêu. Mục đích là để có được một loạt các giá trị khác nhau, không chỉ để thêm phức tạp vì lợi ích của sự phức tạp.


3
Đúng, một hàm băm kém sẽ dẫn đến loại hành vi này. +1
Henning

Không hẳn vậy. Danh sách chỉ được tạo nếu băm giống nhau, nhưng khóa thì khác . Ví dụ: nếu một Chuỗi cung cấp mã băm 2345 và và Số nguyên cung cấp cùng một mã băm 2345, thì số nguyên được chèn vào danh sách vì String.equals( Integer )false. Nhưng nếu bạn có cùng một lớp (hoặc ít nhất .equalstrả về true) thì mục nhập giống nhau sẽ được sử dụng. Ví dụ: new String("one")và `chuỗi mới (" một ") được sử dụng làm khóa, sẽ sử dụng cùng một mục nhập. Trên thực tế, đây là điểm TOÀN BỘ của HashMap ở vị trí đầu tiên! Xem cho chính mình: pastebin.com/f20af40b9
OscarRyz

3
@Oscar: Xem câu trả lời của tôi được thêm vào bài đăng gốc của tôi.
Jay

Tôi biết đây là một chủ đề rất cũ, nhưng đây là một tham chiếu cho thuật ngữ "va chạm" vì nó liên quan đến mã băm: liên kết . Khi bạn thay thế một giá trị trong hashmap bằng cách đặt một giá trị với cùng một khóa, nó được không gọi là va chạm
Tahir Akhtar

@Tahir Chính xác. Có lẽ bài viết của tôi đã được viết kém. Cảm ơn bạn đã làm rõ.
Jay

7

Ý tưởng đầu tiên của tôi là đảm bảo rằng bạn đang khởi tạo HashMap một cách thích hợp. Từ JavaDocs cho HashMap :

Một phiên bản của HashMap có hai tham số ảnh hưởng đến hiệu suất của nó: dung lượng ban đầu và hệ số tải. Dung lượng là số lượng nhóm trong bảng băm và dung lượng ban đầu chỉ đơn giản là dung lượng tại thời điểm bảng băm được tạo. Hệ số tải là thước đo mức độ đầy đủ của bảng băm trước khi dung lượng của nó được tự động tăng lên. Khi số lượng mục nhập trong bảng băm vượt quá tích của hệ số tải và dung lượng hiện tại, bảng băm sẽ được băm lại (nghĩa là cấu trúc dữ liệu nội bộ được xây dựng lại) để bảng băm có số lượng nhóm xấp xỉ gấp đôi.

Vì vậy, nếu bạn đang bắt đầu với một HashMap quá nhỏ, thì mỗi khi nó cần thay đổi kích thước, tất cả các hàm băm sẽ được tính toán lại ... đó có thể là điều bạn cảm thấy khi đạt đến điểm chèn 2-3 triệu.


Tôi không nghĩ rằng chúng được tính toán lại, bao giờ hết. Kích thước bảng được tăng lên, các hàm băm được giữ nguyên.
Henning

Hashmap chỉ thực hiện một chút khôn ngoan và cho mọi mục nhập: newIndex = ManagedHash & newLength;
Henning

4
Hanning: Có lẽ từ ngữ kém về phần của delfuego, nhưng điểm chính xác. Có, các giá trị băm không được tính lại theo nghĩa là đầu ra của hashCode () không được tính toán lại. Nhưng Khi kích thước bảng được tăng lên, tất cả các khóa phải được chèn lại vào bảng, nghĩa là, giá trị băm phải được băm lại để có số vị trí mới trong bảng.
Jay

Jay, vâng - thực sự là từ ngữ kém, và những gì bạn đã nói. :)
delfuego

1
@delfuego và @ nash0: Đúng vậy, việc đặt dung lượng ban đầu bằng số phần tử sẽ làm giảm hiệu suất vì bạn đang có hàng triệu lần va chạm và do đó bạn chỉ sử dụng một lượng nhỏ dung lượng đó. Ngay cả khi bạn sử dụng tất cả các mục có sẵn, việc đặt cùng một dung lượng sẽ khiến nó trở nên tồi tệ nhất !, vì do hệ số tải sẽ yêu cầu nhiều không gian hơn. Bạn sẽ phải sử dụng initialcapactity = maxentries/loadcapacity(chẳng hạn như 30M, 0,95 cho 26 triệu mục nhập) nhưng đây KHÔNG phải là trường hợp của bạn, vì bạn có tất cả những va chạm đó mà bạn chỉ sử dụng khoảng 20k trở xuống.
OscarRyz

7

Tôi muốn đề xuất một cách tiếp cận ba hướng:

  1. Chạy Java với nhiều bộ nhớ hơn: java -Xmx256Mví dụ: chạy với 256 Megabyte. Sử dụng nhiều hơn nếu cần và bạn có nhiều RAM.

  2. Lưu trữ các giá trị băm được tính toán của bạn theo đề xuất của một người đăng khác, vì vậy mỗi đối tượng chỉ tính giá trị băm của nó một lần.

  3. Sử dụng một thuật toán băm tốt hơn. Cái bạn đã đăng sẽ trả về cùng một hàm băm trong đó a = {0, 1} giống như ở đó a = {1, 0}, tất cả những thứ khác đều bằng nhau.

Sử dụng những gì Java cung cấp miễn phí cho bạn.

public int hashCode() {
    return 31 * Arrays.hashCode(a) + Arrays.hashCode(b);
}

Tôi khá chắc rằng điều này có ít cơ hội xảy ra xung đột hơn so với phương pháp hashCode hiện có của bạn, mặc dù nó phụ thuộc vào bản chất chính xác của dữ liệu của bạn.


RAM có thể là cách nhỏ cho các loại bản đồ và mảng này, vì vậy tôi đã nghi ngờ vấn đề giới hạn bộ nhớ.
Gia hạn

7

Đi vào vùng xám của "chủ đề bật / tắt", nhưng cần thiết để loại bỏ sự nhầm lẫn liên quan đến đề xuất của Oscar Reyes rằng nhiều xung đột băm hơn là một điều tốt vì nó làm giảm số lượng phần tử trong HashMap. Tôi có thể hiểu sai những gì Oscar đang nói, nhưng tôi dường như không phải là người duy nhất: kdgregory, delfuego, Nash0, và tôi dường như đều có chung (sai) hiểu biết.

Nếu tôi hiểu Oscar đang nói gì về cùng một lớp với cùng một mã băm, thì anh ấy đề xuất rằng chỉ một phiên bản của một lớp có mã băm nhất định sẽ được chèn vào HashMap. Ví dụ: nếu tôi có một phiên bản SomeClass với mã băm là 1 và một phiên bản thứ hai của SomeClass với mã băm là 1, thì chỉ một phiên bản SomeClass được chèn vào.

Ví dụ về pastebin Java tại http://pastebin.com/f20af40b9 dường như chỉ ra phần trên tóm tắt chính xác những gì Oscar đang đề xuất.

Bất kể sự hiểu biết hay hiểu lầm nào, điều xảy ra là các trường hợp khác nhau của cùng một lớp sẽ không được chèn chỉ một lần vào HashMap nếu chúng có cùng một mã băm - cho đến khi xác định được liệu các khóa có bằng nhau hay không. Hợp đồng mã băm yêu cầu các đối tượng bằng nhau phải có cùng mã băm; tuy nhiên, nó không yêu cầu các đối tượng không bằng nhau phải có các mã băm khác nhau (mặc dù điều này có thể mong muốn vì các lý do khác) [1].

Sau đây là ví dụ pastebin.com/f20af40b9 (mà Oscar đề cập đến ít nhất hai lần), nhưng được sửa đổi một chút để sử dụng xác nhận JUnit thay vì dòng in. Ví dụ này được sử dụng để hỗ trợ đề xuất rằng các mã băm giống nhau gây ra xung đột và khi các lớp giống nhau, chỉ một mục nhập được tạo (ví dụ: chỉ một Chuỗi trong trường hợp cụ thể này):

@Test
public void shouldOverwriteWhenEqualAndHashcodeSame() {
    String s = new String("ese");
    String ese = new String("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // AND equal
    assertTrue(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(2, map.size());

    assertEquals(2, map.get("ese"));
    assertEquals(3, map.get(some));

    assertTrue(s.equals(ese) && s.equals("ese"));
}

class SomeClass {
    public int hashCode() {
        return 100727;
    }
}

Tuy nhiên, mã băm không phải là câu chuyện hoàn chỉnh. Điều mà ví dụ pastebin bỏ qua là thực tế là cả hai seseđều bằng nhau: chúng đều là chuỗi "ese". Do đó, việc chèn hoặc lấy nội dung của bản đồ bằng cách sử dụng shoặc esehoặc "ese"làm khóa đều tương đương vì s.equals(ese) && s.equals("ese").

Thử nghiệm thứ hai cho thấy sai lầm khi kết luận rằng các mã băm giống hệt nhau trên cùng một lớp là lý do khiến khóa -> giá trị s -> 1bị ghi đè ese -> 2khi map.put(ese, 2)được gọi trong thử nghiệm một. Trong thử nghiệm hai, sesevẫn có cùng một mã băm (như được xác minh bởi assertEquals(s.hashCode(), ese.hashCode());) VÀ chúng là cùng một lớp. Tuy nhiên, seselà các MyStringphiên bản trong thử nghiệm này, không phải các Stringphiên bản Java - với sự khác biệt duy nhất liên quan đến thử nghiệm này là bằng: String s equals String esetrong thử nghiệm một ở trên, trong khi MyStrings s does not equal MyString esetrong thử nghiệm hai:

@Test
public void shouldInsertWhenNotEqualAndHashcodeSame() {
    MyString s = new MyString("ese");
    MyString ese = new MyString("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // BUT not equal
    assertFalse(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(3, map.size());

    assertEquals(1, map.get(s));
    assertEquals(2, map.get(ese));
    assertEquals(3, map.get(some));
}

/**
 * NOTE: equals is not overridden so the default implementation is used
 * which means objects are only equal if they're the same instance, whereas
 * the actual Java String class compares the value of its contents.
 */
class MyString {
    String i;

    MyString(String i) {
        this.i = i;
    }

    @Override
    public int hashCode() {
        return 100727;
    }
}

Dựa trên một nhận xét sau đó, Oscar dường như đảo ngược những gì anh ấy đã nói trước đó và thừa nhận tầm quan trọng của sự bình đẳng. Tuy nhiên, có vẻ như khái niệm bình đẳng là điều quan trọng, không phải là "cùng một lớp", là không rõ ràng (tôi nhấn mạnh):

"Không hẳn. Danh sách chỉ được tạo nếu hàm băm giống nhau, nhưng khóa khác nhau. Ví dụ: nếu Chuỗi cung cấp mã băm 2345 và Số nguyên cung cấp cùng mã băm 2345, thì số nguyên được chèn vào danh sách vì Chuỗi. equals (Integer) là false. Nhưng nếu bạn có cùng một lớp (hoặc ít nhất .equals trả về true) thì cùng một mục được sử dụng. Ví dụ: new String ("một") và `new String (" one ") được sử dụng làm , sẽ sử dụng cùng một mục nhập. Trên thực tế, đây là điểm TOÀN BỘ của HashMap ở vị trí đầu tiên! Hãy tự xem: pastebin.com/f20af40b9 - Oscar Reyes "

so với các nhận xét trước đó giải quyết rõ ràng tầm quan trọng của cùng một lớp và cùng một mã băm, không đề cập đến dấu bằng:

"@delfuego: Hãy tự xem: pastebin.com/f20af40b9 Vì vậy, trong câu hỏi này, cùng một lớp đang được sử dụng (chờ một chút, cùng một lớp đang được sử dụng đúng không?). Điều này ngụ ý rằng khi cùng một hàm băm được sử dụng cùng một mục nhập được sử dụng và không có "danh sách" các mục. - Oscar Reyes "

hoặc là

"Trên thực tế, điều này sẽ làm tăng hiệu suất. Càng nhiều va chạm, càng ít mục nhập trong bảng băm. Càng ít công việc phải làm. Không phải hàm băm (trông ổn) cũng không phải bảng băm (hoạt động tốt). Tôi cá rằng nó nằm trên đối tượng sáng tạo mà hiệu suất đang giảm sút. - Oscar Reyes "

hoặc là

"@kdgregory: Có, nhưng chỉ khi va chạm xảy ra với các lớp khác nhau, đối với cùng một lớp (trường hợp này), cùng một mục được sử dụng. - Oscar Reyes"

Một lần nữa, tôi có thể hiểu nhầm những gì Oscar thực sự đang cố gắng nói. Tuy nhiên, những bình luận ban đầu của anh ấy đã gây ra sự nhầm lẫn đến mức có vẻ như cần thận trọng để làm rõ mọi thứ bằng một số thử nghiệm rõ ràng để không có nghi ngờ kéo dài.


[1] - Từ Java hiệu quả, Ấn bản thứ hai của Joshua Bloch:

  • Bất cứ khi nào nó được gọi trên cùng một đối tượng nhiều hơn một lần trong quá trình thực thi một ứng dụng, phương thức hashCode phải luôn trả về cùng một số nguyên, miễn là không có thông tin nào được sử dụng trong các so sánh bằng nhau trên đối tượng bị sửa đổi. Số nguyên này không cần phải duy trì nhất quán từ một lần thực thi một ứng dụng đến một lần thực thi khác của cùng một ứng dụng.

  • Nếu hai đối tượng bằng nhau theo phương thức s (obj ect) bằng nhau, thì việc gọi phương thức hashCode trên mỗi đối tượng trong hai đối tượng phải tạo ra cùng một kết quả số nguyên.

  • Không bắt buộc rằng nếu hai đối tượng là không bằng nhau theo phương thức s (Object) bằng nhau, thì việc gọi phương thức hashCode trên mỗi đối tượng trong hai đối tượng phải tạo ra kết quả số nguyên riêng biệt. Tuy nhiên, lập trình viên nên lưu ý rằng việc tạo ra các kết quả số nguyên riêng biệt cho các đối tượng không bằng nhau có thể cải thiện hiệu suất của bảng băm.


5

Nếu các mảng trong Mã băm đã đăng của bạn là byte, thì bạn có thể sẽ có rất nhiều bản sao.

a [0] + a [1] sẽ luôn nằm trong khoảng từ 0 đến 512. thêm chữ b sẽ luôn dẫn đến một số từ 0 đến 768. nhân chúng và bạn nhận được giới hạn trên là 400.000 kết hợp duy nhất, giả sử dữ liệu của bạn được phân phối hoàn hảo trong số mọi giá trị có thể có của mỗi byte. Nếu dữ liệu của bạn hoàn toàn bình thường, bạn có thể có ít kết quả đầu ra duy nhất của phương pháp này.


4

HashMap có dung lượng ban đầu và hiệu suất của HashMap rất phụ thuộc vào mã băm tạo ra các đối tượng cơ bản.

Cố gắng tinh chỉnh cả hai.


4

Nếu các phím có bất kỳ mẫu nào đối với chúng thì bạn có thể chia bản đồ thành các bản đồ nhỏ hơn và có một bản đồ chỉ mục.

Ví dụ: Các phím: 1,2,3, .... n 28 bản đồ mỗi bản đồ 1 triệu. Bản đồ chỉ mục: 1-1.000.000 -> Bản đồ1 1.000.000-2.000.000 -> Bản đồ2

Vì vậy, bạn sẽ thực hiện hai lần tra cứu nhưng bộ khóa sẽ là 1.000.000 so với 28.000.000. Bạn cũng có thể dễ dàng làm điều này với các mẫu kim tuyến.

Nếu các phím hoàn toàn ngẫu nhiên thì điều này sẽ không hoạt động


1
Ngay cả khi các khóa là ngẫu nhiên, bạn có thể sử dụng (key.hashCode ()% 28) để chọn bản đồ nơi lưu trữ khóa-giá trị đó.
Juha Syrjälä

4

Nếu hai mảng byte mà bạn đề cập là toàn bộ khóa của bạn, các giá trị nằm trong khoảng 0-51, duy nhất và thứ tự trong mảng a và b là không đáng kể, phép toán của tôi cho tôi biết rằng chỉ có khoảng 26 triệu hoán vị khả thi và rằng bạn có thể đang cố gắng điền vào bản đồ các giá trị cho tất cả các khóa có thể có.

Trong trường hợp này, cả việc điền và lấy giá trị từ kho dữ liệu của bạn tất nhiên sẽ nhanh hơn nhiều nếu bạn sử dụng một mảng thay vì HashMap và lập chỉ mục nó từ 0 đến 25989599.


Đó là một ý tưởng rất hay và thực tế là tôi đang làm điều đó cho một vấn đề lưu trữ dữ liệu khác với 1,2 tỷ phần tử. Trong trường hợp này tôi muốn để có những lối thoát dễ dàng và sử dụng một cấu trúc dữ liệu premade :)
nash

4

Tôi đến muộn ở đây, nhưng một vài nhận xét về bản đồ lớn:

  1. Như đã thảo luận ở các bài viết khác, với một hashCode () tốt, 26 triệu mục trong Bản đồ không phải là vấn đề lớn.
  2. Tuy nhiên, một vấn đề tiềm ẩn ở đây là tác động GC của các bản đồ khổng lồ.

Tôi đang đưa ra một giả định rằng những bản đồ này đã tồn tại từ lâu. tức là bạn điền chúng và chúng tồn tại trong suốt thời gian của ứng dụng. Tôi cũng giả định rằng bản thân ứng dụng đã tồn tại lâu - giống như một máy chủ nào đó.

Mỗi mục nhập trong Java HashMap yêu cầu ba đối tượng: khóa, giá trị và Mục nhập liên kết chúng với nhau. Vì vậy, 26 triệu mục trong bản đồ có nghĩa là 26M * 3 == 78M đối tượng. Điều này là tốt cho đến khi bạn đạt đủ GC. Sau đó, bạn có một vấn đề tạm dừng thế giới. GC sẽ xem xét từng vật thể trong số 78M và xác định tất cả chúng đều còn sống. 78 triệu + đối tượng chỉ là rất nhiều đối tượng để xem xét. Nếu ứng dụng của bạn có thể chịu được các lần tạm dừng lâu (có thể nhiều giây) không thường xuyên thì không có vấn đề gì. Nếu bạn đang cố gắng đạt được bất kỳ đảm bảo độ trễ nào, bạn có thể gặp vấn đề lớn (tất nhiên nếu bạn muốn đảm bảo độ trễ, thì Java không phải là nền tảng để chọn :)) Nếu các giá trị trong bản đồ của bạn bị xáo trộn nhanh chóng, bạn có thể kết thúc với việc thu thập đầy đủ thường xuyên mà vấn đề hợp chất rất nhiều.

Tôi không biết một giải pháp tuyệt vời cho vấn đề này. Ý tưởng:

  • Đôi khi có thể điều chỉnh kích thước GC và heap để "hầu hết" ngăn chặn GC đầy đủ.
  • Nếu nội dung bản đồ của bạn bị xáo trộn nhiều, bạn có thể thử FastMap của Javolution - nó có thể gộp các đối tượng Entry, có thể làm giảm tần suất thu thập đầy đủ
  • Bạn có thể tạo bản đồ của riêng mình và quản lý bộ nhớ rõ ràng trên byte [] (tức là giao dịch cpu để có độ trễ dễ đoán hơn bằng cách tuần tự hóa hàng triệu đối tượng thành một byte duy nhất [] - ugh!)
  • Không sử dụng Java cho phần này - nói chuyện với một số loại DB trong bộ nhớ có thể dự đoán được qua một socket
  • Mong rằng bộ sưu tập G1 mới sẽ giúp đỡ (chủ yếu áp dụng cho trường hợp churn cao)

Chỉ là một số suy nghĩ từ một người đã dành nhiều thời gian với các bản đồ khổng lồ trong Java.



3

Trong trường hợp của tôi, tôi muốn tạo một bản đồ với 26 triệu mục nhập. Sử dụng Java HashMap tiêu chuẩn, tốc độ đặt trở nên chậm không thể chịu nổi sau 2-3 triệu lần chèn.

Từ thí nghiệm của tôi (dự án sinh viên năm 2009):

  • Tôi đã xây dựng Red Black Tree cho 100.000 nút từ 1 đến 100.000. Nó mất 785,68 giây (13 phút). Và tôi đã thất bại trong việc xây dựng RBTree cho 1 triệu nút (giống như kết quả của bạn với HashMap).
  • Sử dụng "Cây nguyên tố", cấu trúc dữ liệu thuật toán của tôi. Tôi có thể xây dựng một cây / bản đồ cho 10 triệu nút trong vòng 21,29 giây (RAM: 1,97Gb). Chi phí khóa-giá trị tìm kiếm là O (1).

Lưu ý: "Prime Tree" hoạt động tốt nhất trên "phím liên tục" từ 1 - 10 triệu. Để làm việc với các khóa như HashMap, chúng tôi cần một số điều chỉnh dành cho trẻ nhỏ.


Vậy, #PrimeTree là gì? Nói ngắn gọn, nó là một cấu trúc dữ liệu dạng cây giống như Binary Tree, với số nhánh là số nguyên tố (thay vì "2" -binary).


Bạn có thể vui lòng chia sẻ một số liên kết hoặc triển khai?
Benj

2

Bạn có thể thử sử dụng cơ sở dữ liệu trong bộ nhớ như HSQLDB .



1

Bạn đã cân nhắc việc sử dụng cơ sở dữ liệu nhúng để thực hiện việc này chưa. Nhìn vào Berkeley DB . Nó là mã nguồn mở, hiện thuộc sở hữu của Oracle.

Nó lưu trữ mọi thứ dưới dạng cặp Key-> Value, nó KHÔNG phải là RDBMS. và nó nhằm mục đích nhanh chóng.


2
Berkeley DB không đủ nhanh cho số lượng mục nhập này do chi phí tuần tự hóa / IO; nó không bao giờ có thể nhanh hơn một hashmap và OP không quan tâm đến sự bền bỉ. Đề xuất của bạn không phải là một gợi ý tốt.
oxbow_lakes

1

Trước tiên, bạn nên kiểm tra xem bạn đang sử dụng Map đúng cách, phương thức hashCode () tốt cho các khóa, dung lượng ban đầu cho Bản đồ, triển khai Bản đồ đúng, v.v. như nhiều câu trả lời khác mô tả.

Sau đó, tôi sẽ đề xuất sử dụng một trình biên dịch để xem điều gì đang thực sự xảy ra và thời gian thực hiện được sử dụng ở đâu. Chẳng hạn, phương thức hashCode () có được thực thi hàng tỷ lần không?

Nếu điều đó không hữu ích, làm thế nào về việc sử dụng một cái gì đó như EHCache hoặc memcached ? Có, chúng là sản phẩm dành cho bộ nhớ đệm nhưng bạn có thể định cấu hình chúng để chúng có đủ dung lượng và sẽ không bao giờ loại bỏ bất kỳ giá trị nào khỏi bộ nhớ đệm.

Một tùy chọn khác sẽ là một số công cụ cơ sở dữ liệu có trọng lượng nhẹ hơn SQL RDBMS đầy đủ. Một cái gì đó giống như Berkeley DB , có thể.

Lưu ý rằng cá nhân tôi không có kinh nghiệm về hiệu suất của các sản phẩm này, nhưng chúng có thể đáng để thử.


1

Bạn có thể thử lưu mã băm đã tính vào bộ đệm ẩn vào đối tượng khóa.

Một cái gì đó như thế này:

public int hashCode() {
  if(this.hashCode == null) {
     this.hashCode = computeHashCode();
  }
  return this.hashCode;
}

private int computeHashCode() {
   int hash = 503;
   hash = hash * 5381 + (a[0] + a[1]);
   hash = hash * 5381 + (b[0] + b[1] + b[2]);
   return hash;
}

Tất nhiên bạn phải cẩn thận để không thay đổi nội dung của khóa sau khi mã băm đã được tính toán lần đầu tiên.

Chỉnh sửa: Có vẻ như bộ nhớ đệm chứa các giá trị mã không đáng giá khi bạn chỉ thêm mỗi khóa một lần vào bản đồ. Trong một số tình huống khác, điều này có thể hữu ích.


Như được chỉ ra bên dưới, không có tính toán lại mã băm của các đối tượng trong HashMap khi nó được thay đổi kích thước, vì vậy điều này không giúp bạn được gì.
delfuego

1

Một người đăng khác đã chỉ ra rằng việc triển khai mã băm của bạn sẽ dẫn đến nhiều va chạm do cách bạn thêm các giá trị lại với nhau. Tôi sẵn sàng như vậy, nếu bạn nhìn vào đối tượng HashMap trong trình gỡ lỗi, bạn sẽ thấy rằng bạn có thể có 200 giá trị băm riêng biệt, với chuỗi xô cực kỳ dài.

Nếu bạn luôn có các giá trị trong phạm vi 0..51, thì mỗi giá trị đó sẽ lấy 6 bit để biểu diễn. Nếu bạn luôn có 5 giá trị, bạn có thể tạo mã băm 30 bit với các phép dịch chuyển trái và bổ sung:

    int code = a[0];
    code = (code << 6) + a[1];
    code = (code << 6) + b[0];
    code = (code << 6) + b[1];
    code = (code << 6) + b[2];
    return code;

Việc dịch chuyển sang trái diễn ra nhanh chóng, nhưng sẽ để lại cho bạn các mã băm không được phân phối đều (vì 6 bit ngụ ý một phạm vi 0..63). Một cách thay thế là nhân băm với 51 và cộng từng giá trị. Điều này vẫn sẽ không được phân phối hoàn hảo (ví dụ: {2,0} và {1,52} sẽ va chạm) và sẽ chậm hơn sự thay đổi.

    int code = a[0];
    code *= 51 + a[1];
    code *= 51 + b[0];
    code *= 51 + b[1];
    code *= 51 + b[2];
    return code;

@kdgregory: Tôi đã trả lời về "nhiều va chạm ngụ ý làm việc nhiều hơn" ở một nơi khác :)
OscarRyz

1

Như đã chỉ ra, việc triển khai mã băm của bạn có quá nhiều xung đột và việc sửa chữa nó sẽ dẫn đến hiệu suất tốt. Hơn nữa, bộ nhớ đệm các mã băm và triển khai bằng một cách hiệu quả sẽ hữu ích.

Nếu bạn cần tối ưu hóa hơn nữa:

Theo mô tả của bạn, chỉ có (52 * 51/2) * (52 * 51 * 50/6) = 29304600 khóa khác nhau (trong đó 26000000, tức là khoảng 90%, sẽ có mặt). Do đó, bạn có thể thiết kế một hàm băm mà không có bất kỳ va chạm nào và sử dụng một mảng đơn giản thay vì một bản đồ băm để giữ dữ liệu của bạn, giảm mức tiêu thụ bộ nhớ và tăng tốc độ tra cứu:

T[] array = new T[Key.maxHashCode];

void put(Key k, T value) {
    array[k.hashCode()] = value;

T get(Key k) {
    return array[k.hashCode()];
}

(Nói chung, không thể thiết kế một hàm băm hiệu quả, không có va chạm, phân cụm tốt, đó là lý do tại sao HashMap sẽ chịu được các va chạm, điều này phát sinh một số chi phí)

Giả sử abđược sắp xếp, bạn có thể sử dụng hàm băm sau:

public int hashCode() {
    assert a[0] < a[1]; 
    int ahash = a[1] * a[1] / 2 
              + a[0];

    assert b[0] < b[1] && b[1] < b[2];

    int bhash = b[2] * b[2] * b[2] / 6
              + b[1] * b[1] / 2
              + b[0];
    return bhash * 52 * 52 / 2 + ahash;
}

static final int maxHashCode = 52 * 52 / 2 * 52 * 52 * 52 / 6;  

Tôi nghĩ rằng điều này là không có va chạm. Việc chứng minh điều này được để lại như một bài tập cho người đọc thiên về toán học.


1

Trong Java hiệu quả: Hướng dẫn ngôn ngữ lập trình (Series Java)

Chương 3, bạn có thể tìm thấy các quy tắc tốt để tuân theo khi tính toán hashCode ().

Đặc biệt:

Nếu trường là một mảng, hãy xử lý nó như thể mỗi phần tử là một trường riêng biệt. Nghĩa là, tính toán mã băm cho mỗi phần tử quan trọng bằng cách áp dụng các quy tắc này một cách đệ quy và kết hợp các giá trị này theo bước 2.b. Nếu mọi phần tử trong trường mảng đều quan trọng, bạn có thể sử dụng một trong các phương thức Arrays.hashCode được thêm vào trong bản phát hành 1.5.


0

Phân bổ một bản đồ lớn ngay từ đầu. Nếu bạn biết nó sẽ có 26 triệu mục nhập và bạn có bộ nhớ cho nó, hãy làm a new HashMap(30000000).

Bạn có chắc chắn, bạn có đủ bộ nhớ cho 26 triệu mục nhập với 26 triệu khóa và giá trị? Điều này nghe có vẻ như rất nhiều kỷ niệm với tôi. Bạn có chắc chắn rằng việc thu gom rác vẫn hoạt động tốt ở mốc 2 đến 3 triệu của bạn? Tôi có thể tưởng tượng đó là một nút thắt cổ chai.


2
Ồ, một điều khác. Mã băm của bạn phải được phân phối đồng đều để tránh danh sách lớn được liên kết ở các vị trí đơn lẻ trong bản đồ.
Gia hạn

0

Bạn có thể thử hai điều:

  • Làm cho hashCodephương thức của bạn trả về thứ gì đó đơn giản hơn và hiệu quả hơn, chẳng hạn như một số nguyên liên tiếp

  • Khởi tạo bản đồ của bạn dưới dạng:

    Map map = new HashMap( 30000000, .95f );

Hai hành động đó sẽ làm giảm đáng kể số lần băm lại cấu trúc đang thực hiện và tôi nghĩ là khá dễ kiểm tra.

Nếu điều đó không hiệu quả, hãy xem xét sử dụng một bộ lưu trữ khác một RDBMS.

BIÊN TẬP

Thật kỳ lạ khi thiết lập dung lượng ban đầu làm giảm hiệu suất trong trường hợp của bạn.

Xem từ javadocs :

Nếu dung lượng ban đầu lớn hơn số mục nhập tối đa chia cho hệ số tải, sẽ không có hoạt động rehash nào xảy ra.

Tôi đã tạo một microbeachmark (không phải là xác định của bất kỳ người nào nhưng ít nhất chứng minh được điểm này)

$cat Huge*java
import java.util.*;
public class Huge {
    public static void main( String [] args ) {
        Map map = new HashMap( 30000000 , 0.95f );
        for( int i = 0 ; i < 26000000 ; i ++ ) { 
            map.put( i, i );
        }
    }
}
import java.util.*;
public class Huge2 {
    public static void main( String [] args ) {
        Map map = new HashMap();
        for( int i = 0 ; i < 26000000 ; i ++ ) { 
            map.put( i, i );
        }
    }
}
$time java -Xms2g -Xmx2g Huge

real    0m16.207s
user    0m14.761s
sys 0m1.377s
$time java -Xms2g -Xmx2g Huge2

real    0m21.781s
user    0m20.045s
sys 0m1.656s
$

Vì vậy, việc sử dụng dung lượng ban đầu giảm từ 21 giây xuống 16 giây do việc chia sẻ lại. Điều đó để lại cho chúng tôi hashCodephương pháp của bạn như một "lĩnh vực cơ hội";)

BIÊN TẬP

Không phải là HashMap

Theo ấn bản cuối cùng của bạn.

Tôi nghĩ bạn thực sự nên lập hồ sơ ứng dụng của mình và xem bộ nhớ / cpu đang được sử dụng ở đâu.

Tôi đã tạo một lớp triển khai cùng một lớp của bạn hashCode

Mã băm đó tạo ra hàng triệu lần va chạm, sau đó các mục trong HashMap bị giảm đáng kể.

Tôi vượt từ độ tuổi 21, 16 trong bài kiểm tra trước của mình xuống 10 và 8. Lý do là vì Mã băm gây ra một số lượng lớn các va chạm và bạn không lưu trữ 26 triệu đối tượng như bạn nghĩ mà là một con số thấp hơn đáng kể (tôi sẽ nói khoảng 20k) Vì vậy:

Các vấn đề KHÔNG PHẢI LÀ BẢN ĐỒ HỌA TIẾT nằm ở đâu đó trong mã của bạn.

Đó là thời gian để có được một hồ sơ và tìm ra nơi. Tôi nghĩ rằng đó là do việc tạo ra mục hoặc có thể bạn đang ghi vào đĩa hoặc nhận dữ liệu từ mạng.

Đây là cách tôi triển khai lớp học của bạn.

lưu ý rằng tôi đã không sử dụng phạm vi 0-51 như bạn đã làm nhưng -126 đến 127 cho các giá trị của tôi và thừa nhận đã lặp lại, đó là bởi vì tôi đã làm bài kiểm tra này trước khi bạn cập nhật câu hỏi của mình

Sự khác biệt duy nhất là lớp của bạn sẽ có nhiều va chạm hơn do đó ít vật phẩm được lưu trữ trong bản đồ hơn.

import java.util.*;
public class Item {

    private static byte w = Byte.MIN_VALUE;
    private static byte x = Byte.MIN_VALUE;
    private static byte y = Byte.MIN_VALUE;
    private static byte z = Byte.MIN_VALUE;

    // Just to avoid typing :) 
    private static final byte M = Byte.MAX_VALUE;
    private static final byte m = Byte.MIN_VALUE;


    private byte [] a = new byte[2];
    private byte [] b = new byte[3];

    public Item () {
        // make a different value for the bytes
        increment();
        a[0] = z;        a[1] = y;    
        b[0] = x;        b[1] = w;   b[2] = z;
    }

    private static void increment() {
        z++;
        if( z == M ) {
            z = m;
            y++;
        }
        if( y == M ) {
            y = m;
            x++;
        }
        if( x == M ) {
            x = m;
            w++;
        }
    }
    public String toString() {
        return "" + this.hashCode();
    }



    public int hashCode() {
        int hash = 503;
        hash = hash * 5381 + (a[0] + a[1]);
        hash = hash * 5381 + (b[0] + b[1] + b[2]);
        return hash;
    }
    // I don't realy care about this right now. 
    public boolean equals( Object other ) {
        return this.hashCode() == other.hashCode();
    }

    // print how many collisions do we have in 26M items.
    public static void main( String [] args ) {
        Set set = new HashSet();
        int collisions = 0;
        for ( int i = 0 ; i < 26000000 ; i++ ) {
            if( ! set.add( new Item() ) ) {
                collisions++;
            }
        }
        System.out.println( collisions );
    }
}

Sử dụng lớp này có Khóa cho chương trình trước

 map.put( new Item() , i );

đưa cho tôi:

real     0m11.188s
user     0m10.784s
sys 0m0.261s


real     0m9.348s
user     0m9.071s
sys  0m0.161s

3
Oscar, như đã chỉ ra ở những nơi khác ở trên (theo nhận xét của bạn), bạn dường như cho rằng nhiều va chạm là TỐT; nó rất nhiều KHÔNG tốt. Xung đột có nghĩa là vị trí tại một hàm băm nhất định chuyển từ chứa một mục nhập duy nhất sang chứa danh sách các mục nhập và danh sách này phải được tìm kiếm / duyệt qua mỗi khi vị trí được truy cập.
delfuego

@delfuego: Không thực sự, điều đó chỉ xảy ra khi bạn có va chạm bằng cách sử dụng các lớp khác nhau nhưng đối với cùng một lớp, cùng một mục được sử dụng;)
OscarRyz

2
@Oscar - xem câu trả lời của tôi cho bạn với câu trả lời của MAK. HashMap duy trì một danh sách các mục nhập được liên kết tại mỗi nhóm băm và đi theo danh sách đó gọi bằng () trên mọi phần tử. Lớp của đối tượng không có gì để làm với nó (ngoại trừ ngắn mạch trên bằng ()).
kdgregory

1
@Oscar - Đọc câu trả lời của bạn, có vẻ như bạn đang giả định rằng equals () sẽ trả về true nếu các mã băm giống nhau. Đây không phải là một phần của hợp đồng bằng / mã băm. Nếu tôi hiểu lầm, hãy bỏ qua bình luận này.
kdgregory

1
Cảm ơn bạn rất nhiều vì nỗ lực của Oscar nhưng tôi nghĩ bạn đang nhầm lẫn giữa các đối tượng chính là bằng nhau và có cùng mã băm. Ngoài ra, tại một trong các liên kết mã của bạn, bạn đang sử dụng chuỗi bằng làm khóa, hãy nhớ rằng chuỗi trong Java là bất biến. Tôi nghĩ hôm nay cả hai chúng ta đã học được rất nhiều điều về băm :)
nash 19/11/09


0

Tôi đã thực hiện một thử nghiệm nhỏ trước đây với một danh sách và một bản đồ băm, một điều thú vị là việc lặp lại danh sách và tìm đối tượng mất cùng một khoảng thời gian tính bằng mili giây như khi sử dụng hàm hashmaps get ... chỉ là một fyi. Ồ, bộ nhớ là một vấn đề lớn khi làm việc với các bản đồ băm có kích thước như vậy.


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.