Scala 比 Java 還快?

jopen 10年前發布 | 26K 次閱讀 Scala

Scala 比 Java 還快?

通常Scala被認為比Java要慢,特別是用于函數式編程時。本文會解釋為什么這個被廣泛接受的假設是錯誤的。

數據驗證

編程中一個常見的問題是數據驗證。即我們要確保所有得到的數據處于正確的結構中。我們需要從安全的,編譯器驗證的數據中找到不安全的外部輸入。在一個典型的WEB應用中,你需要驗證每個請求。很明顯這會影響你的應用的性能。在本文中我將會比較處理這個問題的兩種極不相同的解決方案。Java的Bean驗證API和來自play的統一驗證API。后者是一種更為函數式的方法,它具有不變性和類型安全的特性。

Java: Bean 驗證API, aka JSR 303

Bean驗證規范首發于2009年。此API使用注解為JavaBean設置約束。然后你需要在一個注解實例上調用驗證方法來驗證這個Bean的有效性。它的最著名的參考實現來自于Hibernate.

這是他們網址上的一個小示例。:

public class Car {
   @NotNull
   private String manufacturer;

   @NotNull
   @Size(min = 2, max = 14)
   private String licensePlate;

   @Min(2)
   private int seatCount;

   // ...
}

這只是用于聲明,真實的驗證應該像這樣(同樣來自于他們的網址)。

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

Car car = new Car("Ford", "0xCAFEBABE", 2);
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

所以你將一個實例傳遞給validator.validate并獲得一個包含錯誤的Set。如果這個Set是空的,那這個對象就是正確的。

通常你需要用此API來驗證Json和XML.以下是一個解析和驗證Json對象的例子。

public class Car {
   @NotNull
   private String manufacturer;

   @NotNull
   @Size(min = 2, max = 14)
   private String licensePlate;

   @Min(2)
   private int seatCount;

   // getters and setters
}

String json = ... // contains a json string representing a car

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
ObjectMapper mapper = new ObjectMapper(); 
Car car = mapper.readValue(json, Car.class); // use Jackson to unmarshall the json 
Set<ConstraintViolation<Car>> constraintViolations = validator.validate(car);

Scala: Play 統一驗證 API.

這個統一驗證API 致力于提供驗證任何數據結構所需的核心原語。它的主要目的是替代Json驗證API和play框架中的表單驗證API。它易于拓展且支持Json驗證和非傳統的表單驗證。

以下是一個Json驗證場景的一點。注意這次我們在直接驗證Json。

case class Car(manufacturer: String, licensePlate: String, seatCount: Int)

val carValidation = From[JsValue]{ __ =>
   ((__ \ "manufacturer").read[String] ~
    (__ \ "seatCount").read(min(2)) ~
    (__ \ "licensePlate").read(minLength(2) |+| maxLength(14)))(Car.apply _)
}

val json: String = ... // contains a json string representing a car

val validationResult = From[JsObject, Car](json)

在實現此API時,我沒有太注重性能。我的主要目標是正確性,組合性和類型安全。實例上一些設計選擇,例如惰性驗證,都會對性能產生影響。接下來我們看下它是如何執行的。

基準

測試協議

該基準包括解析和驗證存儲在文件的JSON對象. 這些數據是從 the Last.fm Dataset 抽取。JSON的結構已經被修改了一下,便于解析 (使用非常棒的 jq). 代碼托管在 這里

通過使用一系列的scala的計量基準測試進行性能測量. 這兩個API都使用Jackson 解析JSON。

結果

基準用5000到10000的JSON對象解析并且驗證所花費的時間來測量。在兩個不同的場景進行測試:

  • 所有對象都是有效的

  • 所有的對象有一個無效的字段

這是結果。較低的執行時間性能更好。

194516_nikx_95974.png

讓人驚訝的是,Scala的API快很多!

  • 當對象有效時,Scala的API比Java API快1.6倍。

  • 當對象包含一個無效的字段, Scala的API比Java API的速度更快,高達2.6倍Scala API is up 2.6x faster 。

Scala的API相比比Java 的API,明顯更快。 有無效字段將極大地影響Java API中的性能,而對Scala這邊的影響不大。

基準測試不重要

那么我們學到了什么呢?

我們學到了在這特殊設置下,一個用Scala寫的特殊庫比一個用Java寫的特殊哭更快。這并不真的意味著一個Java程序總是比一個Scala程序慢。

從Scala開始以來,我們就看到很多人好奇 是否Scala真的比Java慢.

基準測試得到了更多的關注,  這一個 是特殊的。它演示了一個用cpoll_cppsp 寫的返回json的web程序比nodejs應用快4倍,后者每秒‘只’發送228,887次響應。

我寫了一個實際的Web應用程序。我也貢獻代碼的Web框架。這和我有關嗎?

不完全是。基準測試不會與實際應用有關系。同一個量級上,在一個節點上每秒228887次反應,比任何實際的應用數據都要多。

你可能會問為什么我花時間寫我自己的基準測試?我期待著并希望能證明統一的API能比Java做的更好。與流行的觀念相反,Scala程序可以比其對應的Java更快。

一個有趣的問題是:為什么“慢”的語言速度更快?答案其實很簡單。一個更好的,可擴展的語言提供了更好的工具來寫出更好的程序。

Java在微基準測試上打敗Scala也不要緊。因為Java缺乏必要的構造,它強迫你使用的變通的方法,例如使用flect。這些方法不僅影響了你的程序的性能,更重要的是,他們是使得你的程序運行緩慢的原因。

關于正確復合和用戶友好的一個案例

Java的問題

不友好的API

主要問題在于我使用了Java的validation API,(實際上與Java一樣)不必要這么復雜。當然這個平凡的例子看起來漂亮的和簡單的,但是當你開始鉆研它后,事情就變得很有趣了:

讓我們看一個簡單的例子,跟蹤JSR-303的一個實例:

public class Similar {
   @NotNull
   private String id;
   @Min(0)
   @Max(1)
   private float score;
   // .. getters and setters ...
}

public class Tag {
   @NotNull
   private String id;
   @NotNull
   private String score;
   // .. getters and setters ...
}

public class Track {
   @NotNull
   private String id;
   @NotNull
   private String title;
   @NotNull
   private List<Similar> similars;
   @NotNull
   private DateTime timestamp;
   @NotNull
   private String artist;
   @NotNull
   private List<Tag> tags;
   
   // .. getters and setters ...
}

當然,這些看起來足夠檢查一個給定的跟蹤實例是否是有效的了。

錯了。你看,我們并沒有明確的驗證一個跟蹤實例的有效性,還必須檢查其Tags and Similars的有效性。你需要用@Valid標注每個屬性。

這種行為是不合直覺的。當我不知道一個跟蹤實例的成員是不是有效的時,我無法得它是否有效。

校驗與分解

JSR 303不處理數據的整理與分解。 他只是對類實例的校驗。出于測試目的,我使用Jackson來將Json數據分解為類實例。但問題是,校驗時分解的一部分。我們很難在沒有檢查JSon結構前提下將JSON樹轉化為類實例。 "age"屬性存在嗎?它是Integer類型的嗎?

考慮到這點,Java 工作流看起來很奇怪。當處理Json時你實際上需要處理3中類型的錯誤:

  • Json數據不是結構良好的。

  • Json是有效的,但不是你想要的結構(例如,少了屬性,類型不匹配等等)

  • 沒有滿足你定義的約束

JSR-303僅僅幫助你處理后者。

這個AIP同樣強制你直接使用不合法的實例。我認為如果一個類實例不合法,你最初就不應該創建它。

難以擴展API

Hibernate校驗程序需要手動添加一些列校驗規則(如郵件,長度,非空等)。那如果你需要創建新的校驗約束條件(比如從hibernate文檔中的例子),你該怎么辦?

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {
  String message() default "{com.mycompany.constraints.checkcase}";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
  CaseMode value();
}

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

  private CaseMode caseMode;

  public void initialize(CheckCase constraintAnnotation) {
      this.caseMode = constraintAnnotation.value();
  }

  public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
   if (object == null)
       return true;
   
   if (caseMode == CaseMode.UPPER)
       return object.equals(object.toUpperCase());
   else
       return object.equals(object.toLowerCase());
  }

}

那是大約 25 行代碼。21 行基本上是固定的,所謂純“儀式性”的。有趣的部分僅僅是:

if (caseMode == CaseMode.UPPER)
    return object.equals(object.toUpperCase());
else
    return object.equals(object.toLowerCase());

我們如何使用統一的校驗API進行擴展相同的內容呢?

def checkCase(caseMode: CaseMode) = validateWith("constraints.checkcase") { (s: String) =>
   if (caseMode == CaseMode.UPPER)
      s == s.toUpperCase
   else
      s == s.toLowerCase
}

是的,這里只有6行代碼。我有自信任何人都可以理解它。

現在,如果你有一個min規則,一個max規則,我想創建一個between規則,該怎么做?這很簡單,規則組合。

def between(lower: Int, upper: Int) = min(lower) |+| max(upper)

然而在Java中呢?好吧,我連寫這些代碼都不想寫。這肯定會冗長并且乏味。

Scala福利: 易于并行

作為一個小福利,既然Scala API只是用不可變的數據結構,那么它就是線程安全的。Scala 提供并行集合。讓我們花5分鐘改一些最初的代碼,我提出了一個發揮我多核處理器最大性能的版本。

時間校驗API測定Hibernate validator的基準點 - invalids Hibernate validator Unified - invalids Unified Unified w/ par - invalids Unified w/ par 5000 6000 7000 8000 9000 10000 0k 1k 2k 3k 4k 5k Highcharts.com

這一版本比Java的快了5倍。

結論

我認為這篇文章的觀點相當明顯。當提到選擇一個庫或者一門語言時,基準測試毫無用處。一個正確實現的算法總是能被優化。一個失敗的實現就是失敗了。

這一特殊的實例證明即使Scala有一些開銷,但是它優秀的設計使得創建一些相比Java不僅簡單而且高效的庫稱為可能。

你可以閱讀 這篇文章 來了解更多關于unified API或者從這里檢出代碼.

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!